View Javadoc
1   package org.argeo.cms.auth;
2   
3   import static org.argeo.naming.LdapAttrs.cn;
4   
5   import java.io.IOException;
6   import java.security.PrivilegedAction;
7   import java.util.Arrays;
8   import java.util.HashSet;
9   import java.util.List;
10  import java.util.Locale;
11  import java.util.Map;
12  import java.util.Set;
13  
14  import javax.naming.ldap.LdapName;
15  import javax.security.auth.Subject;
16  import javax.security.auth.callback.Callback;
17  import javax.security.auth.callback.CallbackHandler;
18  import javax.security.auth.callback.LanguageCallback;
19  import javax.security.auth.callback.NameCallback;
20  import javax.security.auth.callback.PasswordCallback;
21  import javax.security.auth.callback.UnsupportedCallbackException;
22  import javax.security.auth.kerberos.KerberosPrincipal;
23  import javax.security.auth.login.CredentialNotFoundException;
24  import javax.security.auth.login.LoginException;
25  import javax.security.auth.spi.LoginModule;
26  import javax.servlet.http.HttpServletRequest;
27  
28  import org.apache.commons.logging.Log;
29  import org.apache.commons.logging.LogFactory;
30  import org.argeo.api.NodeConstants;
31  import org.argeo.api.security.CryptoKeyring;
32  import org.argeo.cms.CmsException;
33  import org.argeo.cms.internal.kernel.Activator;
34  import org.argeo.naming.LdapAttrs;
35  import org.argeo.osgi.useradmin.AuthenticatingUser;
36  import org.argeo.osgi.useradmin.IpaUtils;
37  import org.argeo.osgi.useradmin.OsUserUtils;
38  import org.argeo.osgi.useradmin.TokenUtils;
39  import org.osgi.framework.BundleContext;
40  import org.osgi.framework.FrameworkUtil;
41  import org.osgi.framework.ServiceReference;
42  import org.osgi.service.useradmin.Authorization;
43  import org.osgi.service.useradmin.Group;
44  import org.osgi.service.useradmin.User;
45  import org.osgi.service.useradmin.UserAdmin;
46  
47  /** Use the {@link UserAdmin} in the OSGi registry as the basis for authentication.*/
48  public class UserAdminLoginModule implements LoginModule {
49  	private final static Log log = LogFactory.getLog(UserAdminLoginModule.class);
50  
51  	private Subject subject;
52  	private CallbackHandler callbackHandler;
53  	private Map<String, Object> sharedState = null;
54  
55  	private List<String> indexedUserProperties = Arrays
56  			.asList(new String[] { LdapAttrs.mail.name(), LdapAttrs.uid.name(), LdapAttrs.authPassword.name() });
57  
58  	// private state
59  	private BundleContext bc;
60  	private User authenticatedUser = null;
61  	private Locale locale;
62  
63  	private Authorization bindAuthorization = null;
64  
65  	private boolean singleUser = Activator.isSingleUser();
66  
67  	@SuppressWarnings("unchecked")
68  	@Override
69  	public void initialize(Subject subject, CallbackHandler callbackHandler, Map<String, ?> sharedState,
70  			Map<String, ?> options) {
71  		this.subject = subject;
72  		try {
73  			bc = FrameworkUtil.getBundle(UserAdminLoginModule.class).getBundleContext();
74  			this.callbackHandler = callbackHandler;
75  			this.sharedState = (Map<String, Object>) sharedState;
76  		} catch (Exception e) {
77  			throw new CmsException("Cannot initialize login module", e);
78  		}
79  	}
80  
81  	@Override
82  	public boolean login() throws LoginException {
83  		UserAdmin userAdmin = Activator.getUserAdmin();
84  		final String username;
85  		final char[] password;
86  		Object certificateChain = null;
87  		boolean preauth = false;
88  		if (sharedState.containsKey(CmsAuthUtils.SHARED_STATE_NAME)
89  				&& sharedState.containsKey(CmsAuthUtils.SHARED_STATE_PWD)) {
90  			// NB: required by Basic http auth
91  			username = (String) sharedState.get(CmsAuthUtils.SHARED_STATE_NAME);
92  			password = (char[]) sharedState.get(CmsAuthUtils.SHARED_STATE_PWD);
93  			// // TODO locale?
94  		} else if (sharedState.containsKey(CmsAuthUtils.SHARED_STATE_NAME)
95  				&& sharedState.containsKey(CmsAuthUtils.SHARED_STATE_CERTIFICATE_CHAIN)) {
96  			String certDn = (String) sharedState.get(CmsAuthUtils.SHARED_STATE_NAME);
97  //			LdapName ldapName;
98  //			try {
99  //				ldapName = new LdapName(certificateName);
100 //			} catch (InvalidNameException e) {
101 //				e.printStackTrace();
102 //				return false;
103 //			}
104 //			username = ldapName.getRdn(ldapName.size() - 1).getValue().toString();
105 			username = certDn;
106 			certificateChain = sharedState.get(CmsAuthUtils.SHARED_STATE_CERTIFICATE_CHAIN);
107 			password = null;
108 		} else if (sharedState.containsKey(CmsAuthUtils.SHARED_STATE_NAME)
109 				&& sharedState.containsKey(CmsAuthUtils.SHARED_STATE_REMOTE_ADDR)
110 				&& sharedState.containsKey(CmsAuthUtils.SHARED_STATE_REMOTE_PORT)) {// ident
111 			username = (String) sharedState.get(CmsAuthUtils.SHARED_STATE_NAME);
112 			password = null;
113 			preauth = true;
114 		} else if (singleUser) {
115 			username = OsUserUtils.getOsUsername();
116 			password = null;
117 		} else {
118 
119 			// ask for username and password
120 			NameCallback nameCallback = new NameCallback("User");
121 			PasswordCallback passwordCallback = new PasswordCallback("Password", false);
122 			LanguageCallback langCallback = new LanguageCallback();
123 			try {
124 				callbackHandler.handle(new Callback[] { nameCallback, passwordCallback, langCallback });
125 			} catch (IOException e) {
126 				throw new LoginException("Cannot handle callback: " + e.getMessage());
127 			} catch (UnsupportedCallbackException e) {
128 				return false;
129 			}
130 
131 			// i18n
132 			locale = langCallback.getLocale();
133 			if (locale == null)
134 				locale = Locale.getDefault();
135 			// FIXME add it to Subject
136 			// Locale.setDefault(locale);
137 
138 			username = nameCallback.getName();
139 			if (username == null || username.trim().equals("")) {
140 				// authorization = userAdmin.getAuthorization(null);
141 				throw new CredentialNotFoundException("No credentials provided");
142 			}
143 			if (passwordCallback.getPassword() != null)
144 				password = passwordCallback.getPassword();
145 			else
146 				throw new CredentialNotFoundException("No credentials provided");
147 			sharedState.put(CmsAuthUtils.SHARED_STATE_NAME, username);
148 			sharedState.put(CmsAuthUtils.SHARED_STATE_PWD, password);
149 		}
150 		User user = searchForUser(userAdmin, username);
151 
152 		// Tokens
153 		if (user == null) {
154 			String token = username;
155 			Group tokenGroup = searchForToken(userAdmin, token);
156 			if (tokenGroup != null) {
157 				Authorization tokenAuthorization = getAuthorizationFromToken(userAdmin, tokenGroup);
158 				if (tokenAuthorization != null) {
159 					bindAuthorization = tokenAuthorization;
160 					authenticatedUser = (User) userAdmin.getRole(bindAuthorization.getName());
161 					return true;
162 				}
163 			}
164 		}
165 
166 		if (user == null)
167 			return true;// expect Kerberos
168 
169 		if (password != null) {
170 			// try bind first
171 			try {
172 				AuthenticatingUser authenticatingUser = new AuthenticatingUser(user.getName(), password);
173 				bindAuthorization = userAdmin.getAuthorization(authenticatingUser);
174 				// TODO check tokens as well
175 				if (bindAuthorization != null) {
176 					authenticatedUser = user;
177 					return true;
178 				}
179 			} catch (Exception e) {
180 				// silent
181 				if (log.isTraceEnabled())
182 					log.trace("Bind failed", e);
183 			}
184 
185 			// works only if a connection password is provided
186 			if (!user.hasCredential(null, password)) {
187 				return false;
188 			}
189 		} else if (certificateChain != null) {
190 			// TODO check CRLs/OSCP validity?
191 			// NB: authorization in commit() will work only if an LDAP connection password
192 			// is provided
193 		} else if (singleUser) {
194 			// TODO verify IP address?
195 		} else if (preauth) {
196 			// ident
197 		} else {
198 			throw new CredentialNotFoundException("No credentials provided");
199 		}
200 
201 		authenticatedUser = user;
202 		return true;
203 	}
204 
205 	@Override
206 	public boolean commit() throws LoginException {
207 		if (locale == null)
208 			subject.getPublicCredentials().add(Locale.getDefault());
209 		else
210 			subject.getPublicCredentials().add(locale);
211 
212 		if (singleUser) {
213 			OsUserUtils.loginAsSystemUser(subject);
214 		}
215 		UserAdmin userAdmin = Activator.getUserAdmin();
216 		Authorization authorization;
217 		if (callbackHandler == null) {// anonymous
218 			authorization = userAdmin.getAuthorization(null);
219 		} else if (bindAuthorization != null) {// bind
220 			authorization = bindAuthorization;
221 		} else {// Kerberos
222 			User authenticatingUser;
223 			Set<KerberosPrincipal> kerberosPrincipals = subject.getPrincipals(KerberosPrincipal.class);
224 			if (kerberosPrincipals.isEmpty()) {
225 				if (authenticatedUser == null) {
226 					if (log.isTraceEnabled())
227 						log.trace("Neither kerberos nor user admin login succeeded. Login failed.");
228 					return false;
229 				} else {
230 					authenticatingUser = authenticatedUser;
231 				}
232 			} else {
233 				KerberosPrincipal kerberosPrincipal = kerberosPrincipals.iterator().next();
234 				LdapName dn = IpaUtils.kerberosToDn(kerberosPrincipal.getName());
235 				authenticatingUser = new AuthenticatingUser(dn);
236 				if (authenticatedUser != null && !authenticatingUser.getName().equals(authenticatedUser.getName()))
237 					throw new LoginException("Kerberos login " + authenticatingUser.getName()
238 							+ " is inconsistent with user admin login " + authenticatedUser.getName());
239 			}
240 			authorization = Subject.doAs(subject, new PrivilegedAction<Authorization>() {
241 
242 				@Override
243 				public Authorization run() {
244 					Authorization authorization = userAdmin.getAuthorization(authenticatingUser);
245 					return authorization;
246 				}
247 
248 			});
249 			if (authorization == null)
250 				throw new LoginException(
251 						"User admin found no authorization for authenticated user " + authenticatingUser.getName());
252 		}
253 
254 		// Log and monitor new login
255 		HttpServletRequest request = (HttpServletRequest) sharedState.get(CmsAuthUtils.SHARED_STATE_HTTP_REQUEST);
256 		CmsAuthUtils.addAuthorization(subject, authorization);
257 
258 		// Unlock keyring (underlying login to the JCR repository)
259 		char[] password = (char[]) sharedState.get(CmsAuthUtils.SHARED_STATE_PWD);
260 		if (password != null) {
261 			ServiceReference<CryptoKeyring> keyringSr = bc.getServiceReference(CryptoKeyring.class);
262 			if (keyringSr != null) {
263 				CryptoKeyring keyring = bc.getService(keyringSr);
264 				Subject.doAs(subject, new PrivilegedAction<Void>() {
265 
266 					@Override
267 					public Void run() {
268 						try {
269 							keyring.unlock(password);
270 						} catch (Exception e) {
271 							e.printStackTrace();
272 							log.warn("Could not unlock keyring with the password provided by " + authorization.getName()
273 									+ ": " + e.getMessage());
274 						}
275 						return null;
276 					}
277 
278 				});
279 			}
280 		}
281 
282 		// Register CmsSession with initial subject
283 		CmsAuthUtils.registerSessionAuthorization(request, subject, authorization, locale);
284 
285 		if (log.isDebugEnabled())
286 			log.debug("Logged in to CMS: " + subject);
287 		return true;
288 	}
289 
290 	@Override
291 	public boolean abort() throws LoginException {
292 		return true;
293 	}
294 
295 	@Override
296 	public boolean logout() throws LoginException {
297 		if (log.isTraceEnabled())
298 			log.trace("Logging out from CMS... " + subject);
299 		// boolean httpSessionLogoutOk = CmsAuthUtils.logoutSession(bc,
300 		// subject);
301 		CmsAuthUtils.cleanUp(subject);
302 		return true;
303 	}
304 
305 	protected User searchForUser(UserAdmin userAdmin, String providedUsername) {
306 		try {
307 			// TODO check value null or empty
308 			Set<User> collectedUsers = new HashSet<>();
309 			// try dn
310 			User user = null;
311 			// try all indexes
312 			for (String attr : indexedUserProperties) {
313 				user = userAdmin.getUser(attr, providedUsername);
314 				if (user != null)
315 					collectedUsers.add(user);
316 			}
317 			if (collectedUsers.size() == 1) {
318 				user = collectedUsers.iterator().next();
319 				return user;
320 			} else if (collectedUsers.size() > 1) {
321 				log.warn(collectedUsers.size() + " users for provided username" + providedUsername);
322 			}
323 			// try DN as a last resort
324 			try {
325 				user = (User) userAdmin.getRole(providedUsername);
326 				if (user != null)
327 					return user;
328 			} catch (Exception e) {
329 				// silent
330 			}
331 			return null;
332 		} catch (Exception e) {
333 			if (log.isTraceEnabled())
334 				log.warn("Cannot search for user " + providedUsername, e);
335 			return null;
336 		}
337 
338 	}
339 
340 	protected Group searchForToken(UserAdmin userAdmin, String token) {
341 		String dn = cn + "=" + token + "," + NodeConstants.TOKENS_BASEDN;
342 		Group tokenGroup = (Group) userAdmin.getRole(dn);
343 		return tokenGroup;
344 	}
345 
346 	protected Authorization getAuthorizationFromToken(UserAdmin userAdmin, Group tokenGroup) {
347 		if (TokenUtils.isExpired(tokenGroup))
348 			return null;
349 //		String expiryDateStr = (String) tokenGroup.getProperties().get(description.name());
350 //		if (expiryDateStr != null) {
351 //			Instant expiryDate = NamingUtils.ldapDateToInstant(expiryDateStr);
352 //			if (expiryDate.isBefore(Instant.now())) {
353 //				if (log.isDebugEnabled())
354 //					log.debug("Token " + tokenGroup.getName() + " has expired.");
355 //				return null;
356 //			}
357 //		}
358 		String userDn = TokenUtils.userDn(tokenGroup);
359 		User user = (User) userAdmin.getRole(userDn);
360 		Authorization auth = userAdmin.getAuthorization(user);
361 		return auth;
362 	}
363 }