View Javadoc
1   /*
2    * MIT License
3    *
4    * Copyright (c) 2010-2024 The Waffle Project Contributors: https://github.com/Waffle/waffle/graphs/contributors
5    *
6    * Permission is hereby granted, free of charge, to any person obtaining a copy
7    * of this software and associated documentation files (the "Software"), to deal
8    * in the Software without restriction, including without limitation the rights
9    * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10   * copies of the Software, and to permit persons to whom the Software is
11   * furnished to do so, subject to the following conditions:
12   *
13   * The above copyright notice and this permission notice shall be included in all
14   * copies or substantial portions of the Software.
15   *
16   * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17   * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18   * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19   * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20   * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21   * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22   * SOFTWARE.
23   */
24  package waffle.jaas;
25  
26  import java.io.IOException;
27  import java.security.Principal;
28  import java.util.ArrayList;
29  import java.util.LinkedHashSet;
30  import java.util.List;
31  import java.util.Locale;
32  import java.util.Map;
33  import java.util.Set;
34  
35  import javax.security.auth.Subject;
36  import javax.security.auth.callback.Callback;
37  import javax.security.auth.callback.CallbackHandler;
38  import javax.security.auth.callback.NameCallback;
39  import javax.security.auth.callback.PasswordCallback;
40  import javax.security.auth.callback.UnsupportedCallbackException;
41  import javax.security.auth.login.LoginException;
42  import javax.security.auth.spi.LoginModule;
43  
44  import org.slf4j.Logger;
45  import org.slf4j.LoggerFactory;
46  
47  import waffle.windows.auth.IWindowsAccount;
48  import waffle.windows.auth.IWindowsAuthProvider;
49  import waffle.windows.auth.IWindowsIdentity;
50  import waffle.windows.auth.PrincipalFormat;
51  import waffle.windows.auth.impl.WindowsAuthProviderImpl;
52  
53  /**
54   * A Java Security login module for Windows authentication.
55   *
56   * @see javax.security.auth.spi.LoginModule
57   */
58  public class WindowsLoginModule implements LoginModule {
59  
60      /** The Constant LOGGER. */
61      private static final Logger LOGGER = LoggerFactory.getLogger(WindowsLoginModule.class);
62  
63      /** The username. */
64      private String username;
65  
66      /** The debug. */
67      private boolean debug;
68  
69      /** The subject. */
70      private Subject subject;
71  
72      /** The callback handler. */
73      private CallbackHandler callbackHandler;
74  
75      /** The auth. */
76      private IWindowsAuthProvider auth = new WindowsAuthProviderImpl();
77  
78      /** The principals. */
79      private Set<Principal> principals;
80  
81      /** The principal format. */
82      private PrincipalFormat principalFormat = PrincipalFormat.FQN;
83  
84      /** The role format. */
85      private PrincipalFormat roleFormat = PrincipalFormat.FQN;
86  
87      /** The allow guest login. */
88      private boolean allowGuestLogin = true;
89  
90      @Override
91      public void initialize(final Subject initSubject, final CallbackHandler initCallbackHandler,
92              final Map<String, ?> initSharedState, final Map<String, ?> initOptions) {
93  
94          this.subject = initSubject;
95          this.callbackHandler = initCallbackHandler;
96  
97          for (final Map.Entry<String, ?> option : initOptions.entrySet()) {
98              if ("debug".equalsIgnoreCase(option.getKey())) {
99                  this.debug = Boolean.parseBoolean((String) option.getValue());
100             } else if ("principalFormat".equalsIgnoreCase(option.getKey())) {
101                 this.principalFormat = PrincipalFormat
102                         .valueOf(((String) option.getValue()).toUpperCase(Locale.ENGLISH));
103             } else if ("roleFormat".equalsIgnoreCase(option.getKey())) {
104                 this.roleFormat = PrincipalFormat.valueOf(((String) option.getValue()).toUpperCase(Locale.ENGLISH));
105             }
106         }
107     }
108 
109     /**
110      * Use Windows SSPI to authenticate a username with a password.
111      *
112      * @return true, if successful
113      *
114      * @throws LoginException
115      *             the login exception
116      */
117     @Override
118     public boolean login() throws LoginException {
119         if (this.callbackHandler == null) {
120             throw new LoginException("Missing callback to gather information from the user.");
121         }
122 
123         final NameCallback usernameCallback = new NameCallback("user name: ");
124         final PasswordCallback passwordCallback = new PasswordCallback("password: ", false);
125 
126         final Callback[] callbacks = new Callback[2];
127         callbacks[0] = usernameCallback;
128         callbacks[1] = passwordCallback;
129 
130         final String userName;
131         final String password;
132 
133         try {
134             this.callbackHandler.handle(callbacks);
135             userName = usernameCallback.getName();
136             password = passwordCallback.getPassword() == null ? "" : new String(passwordCallback.getPassword());
137             passwordCallback.clearPassword();
138         } catch (final IOException e) {
139             WindowsLoginModule.LOGGER.trace("", e);
140             throw new LoginException(e.toString());
141         } catch (final UnsupportedCallbackException e) {
142             WindowsLoginModule.LOGGER.trace("", e);
143             throw new LoginException("Callback {} not available to gather authentication information from the user."
144                     .replace("{}", e.getCallback().getClass().getName()));
145         }
146 
147         IWindowsIdentity windowsIdentity;
148         try {
149             windowsIdentity = this.auth.logonUser(userName, password);
150         } catch (final Exception e) {
151             WindowsLoginModule.LOGGER.trace("", e);
152             throw new LoginException(e.getMessage());
153         }
154 
155         try {
156             // disable guest login
157             if (!this.allowGuestLogin && windowsIdentity.isGuest()) {
158                 WindowsLoginModule.LOGGER.debug("guest login disabled: {}", windowsIdentity.getFqn());
159                 throw new LoginException("Guest login disabled");
160             }
161 
162             this.principals = new LinkedHashSet<>();
163             // add the main user principal to the subject principals
164             this.principals.addAll(WindowsLoginModule.getUserPrincipals(windowsIdentity, this.principalFormat));
165             if (this.roleFormat != PrincipalFormat.NONE) {
166                 // create the group principal and add roles as members of the group
167                 final GroupPrincipal groupList = new GroupPrincipal("Roles");
168                 for (final IWindowsAccount group : windowsIdentity.getGroups()) {
169                     this.principals.addAll(WindowsLoginModule.getRolePrincipals(group, this.roleFormat));
170                     for (final Principal role : WindowsLoginModule.getRolePrincipals(group, this.roleFormat)) {
171                         WindowsLoginModule.LOGGER.debug(" group: {}", role.getName());
172                         groupList.addMember(new RolePrincipal(role.getName()));
173                     }
174                 }
175                 // add the group and roles to the subject principals
176                 this.principals.add(groupList);
177             }
178 
179             this.username = windowsIdentity.getFqn();
180             WindowsLoginModule.LOGGER.debug("successfully logged in {} ({})", this.username,
181                     windowsIdentity.getSidString());
182         } finally {
183             windowsIdentity.dispose();
184         }
185 
186         return true;
187     }
188 
189     /**
190      * Abort a login process.
191      *
192      * @return true, if successful
193      *
194      * @throws LoginException
195      *             the login exception
196      */
197     @Override
198     public boolean abort() throws LoginException {
199         return this.logout();
200     }
201 
202     /**
203      * Commit principals to the subject.
204      *
205      * @return true, if successful
206      *
207      * @throws LoginException
208      *             the login exception
209      */
210     @Override
211     public boolean commit() throws LoginException {
212         if (this.principals == null) {
213             return false;
214         }
215 
216         if (this.subject.isReadOnly()) {
217             throw new LoginException("Subject cannot be read-only.");
218         }
219 
220         final Set<Principal> principalsSet = this.subject.getPrincipals();
221         principalsSet.addAll(this.principals);
222 
223         WindowsLoginModule.LOGGER.debug("committing {} principals",
224                 Integer.valueOf(this.subject.getPrincipals().size()));
225         if (this.debug) {
226             for (final Principal principal : principalsSet) {
227                 WindowsLoginModule.LOGGER.debug(" principal: {}", principal.getName());
228             }
229         }
230 
231         return true;
232     }
233 
234     /**
235      * Logout a user.
236      *
237      * @return true, if successful
238      *
239      * @throws LoginException
240      *             the login exception
241      */
242     @Override
243     public boolean logout() throws LoginException {
244         if (this.subject.isReadOnly()) {
245             throw new LoginException("Subject cannot be read-only.");
246         }
247 
248         this.subject.getPrincipals().clear();
249 
250         if (this.username != null) {
251             WindowsLoginModule.LOGGER.debug("logging out {}", this.username);
252         }
253 
254         return true;
255     }
256 
257     /**
258      * True if Debug is enabled.
259      *
260      * @return True or false.
261      */
262     public boolean isDebug() {
263         return this.debug;
264     }
265 
266     /**
267      * Windows auth provider.
268      *
269      * @return IWindowsAuthProvider.
270      */
271     public IWindowsAuthProvider getAuth() {
272         return this.auth;
273     }
274 
275     /**
276      * Set Windows auth provider.
277      *
278      * @param provider
279      *            Class implements IWindowsAuthProvider.
280      */
281     public void setAuth(final IWindowsAuthProvider provider) {
282         this.auth = provider;
283     }
284 
285     /**
286      * Returns a list of user principal objects.
287      *
288      * @param windowsIdentity
289      *            Windows identity.
290      * @param principalFormat
291      *            Principal format.
292      *
293      * @return A list of user principal objects.
294      */
295     private static List<Principal> getUserPrincipals(final IWindowsIdentity windowsIdentity,
296             final PrincipalFormat principalFormat) {
297 
298         final List<Principal> principalsList = new ArrayList<>();
299         switch (principalFormat) {
300             case FQN:
301                 principalsList.add(new UserPrincipal(windowsIdentity.getFqn()));
302                 break;
303             case SID:
304                 principalsList.add(new UserPrincipal(windowsIdentity.getSidString()));
305                 break;
306             case BOTH:
307                 principalsList.add(new UserPrincipal(windowsIdentity.getFqn()));
308                 principalsList.add(new UserPrincipal(windowsIdentity.getSidString()));
309                 break;
310             case NONE:
311             default:
312                 break;
313         }
314         return principalsList;
315     }
316 
317     /**
318      * Returns a list of role principal objects.
319      *
320      * @param group
321      *            Windows group.
322      * @param principalFormat
323      *            Principal format.
324      *
325      * @return List of role principal objects.
326      */
327     private static List<Principal> getRolePrincipals(final IWindowsAccount group,
328             final PrincipalFormat principalFormat) {
329 
330         final List<Principal> principalsList = new ArrayList<>();
331         switch (principalFormat) {
332             case FQN:
333                 principalsList.add(new RolePrincipal(group.getFqn()));
334                 break;
335             case SID:
336                 principalsList.add(new RolePrincipal(group.getSidString()));
337                 break;
338             case BOTH:
339                 principalsList.add(new RolePrincipal(group.getFqn()));
340                 principalsList.add(new RolePrincipal(group.getSidString()));
341                 break;
342             case NONE:
343                 break;
344             default:
345                 break;
346         }
347         return principalsList;
348     }
349 
350     /**
351      * True if Guest login permitted.
352      *
353      * @return True if Guest login permitted, false otherwise.
354      */
355     public boolean isAllowGuestLogin() {
356         return this.allowGuestLogin;
357     }
358 
359     /**
360      * Set whether Guest login is permitted. Default is true, if the Guest account is enabled, an invalid
361      * username/password results in a Guest login.
362      *
363      * @param value
364      *            True or false.
365      */
366     public void setAllowGuestLogin(final boolean value) {
367         this.allowGuestLogin = value;
368     }
369 }