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