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                 // add Windows Groups as role principles
167                 for (final IWindowsAccount group : windowsIdentity.getGroups()) {
168                     this.principals.addAll(getRolePrincipals(group, this.roleFormat));
169                 }
170             }
171 
172             this.username = windowsIdentity.getFqn();
173             WindowsLoginModule.LOGGER.debug("successfully logged in {} ({})", this.username,
174                     windowsIdentity.getSidString());
175         } finally {
176             windowsIdentity.dispose();
177         }
178 
179         return true;
180     }
181 
182     /**
183      * Abort a login process.
184      *
185      * @return true, if successful
186      *
187      * @throws LoginException
188      *             the login exception
189      */
190     @Override
191     public boolean abort() throws LoginException {
192         return this.logout();
193     }
194 
195     /**
196      * Commit principals to the subject.
197      *
198      * @return true, if successful
199      *
200      * @throws LoginException
201      *             the login exception
202      */
203     @Override
204     public boolean commit() throws LoginException {
205         if (this.principals == null) {
206             return false;
207         }
208 
209         if (this.subject.isReadOnly()) {
210             throw new LoginException("Subject cannot be read-only.");
211         }
212 
213         final Set<Principal> principalsSet = this.subject.getPrincipals();
214         principalsSet.addAll(this.principals);
215 
216         WindowsLoginModule.LOGGER.debug("committing {} principals",
217                 Integer.valueOf(this.subject.getPrincipals().size()));
218         if (this.debug) {
219             for (final Principal principal : principalsSet) {
220                 WindowsLoginModule.LOGGER.debug(" principal: {}", principal.getName());
221             }
222         }
223 
224         return true;
225     }
226 
227     /**
228      * Logout a user.
229      *
230      * @return true, if successful
231      *
232      * @throws LoginException
233      *             the login exception
234      */
235     @Override
236     public boolean logout() throws LoginException {
237         if (this.subject.isReadOnly()) {
238             throw new LoginException("Subject cannot be read-only.");
239         }
240 
241         this.subject.getPrincipals().clear();
242 
243         if (this.username != null) {
244             WindowsLoginModule.LOGGER.debug("logging out {}", this.username);
245         }
246 
247         return true;
248     }
249 
250     /**
251      * True if Debug is enabled.
252      *
253      * @return True or false.
254      */
255     public boolean isDebug() {
256         return this.debug;
257     }
258 
259     /**
260      * Windows auth provider.
261      *
262      * @return IWindowsAuthProvider.
263      */
264     public IWindowsAuthProvider getAuth() {
265         return this.auth;
266     }
267 
268     /**
269      * Set Windows auth provider.
270      *
271      * @param provider
272      *            Class implements IWindowsAuthProvider.
273      */
274     public void setAuth(final IWindowsAuthProvider provider) {
275         this.auth = provider;
276     }
277 
278     /**
279      * Returns a list of user principal objects.
280      *
281      * @param windowsIdentity
282      *            Windows identity.
283      * @param principalFormat
284      *            Principal format.
285      *
286      * @return A list of user principal objects.
287      */
288     private static List<Principal> getUserPrincipals(final IWindowsIdentity windowsIdentity,
289             final PrincipalFormat principalFormat) {
290 
291         final List<Principal> principalsList = new ArrayList<>();
292         switch (principalFormat) {
293             case FQN:
294                 principalsList.add(new UserPrincipal(windowsIdentity.getFqn()));
295                 break;
296             case SID:
297                 principalsList.add(new UserPrincipal(windowsIdentity.getSidString()));
298                 break;
299             case BOTH:
300                 principalsList.add(new UserPrincipal(windowsIdentity.getFqn()));
301                 principalsList.add(new UserPrincipal(windowsIdentity.getSidString()));
302                 break;
303             case NONE:
304             default:
305                 break;
306         }
307         return principalsList;
308     }
309 
310     /**
311      * Returns a list of role principal objects.
312      *
313      * @param group
314      *            Windows group.
315      * @param principalFormat
316      *            Principal format.
317      *
318      * @return List of role principal objects.
319      */
320     private static List<Principal> getRolePrincipals(final IWindowsAccount group,
321             final PrincipalFormat principalFormat) {
322 
323         final List<Principal> principalsList = new ArrayList<>();
324         switch (principalFormat) {
325             case FQN:
326                 principalsList.add(new RolePrincipal(group.getFqn()));
327                 break;
328             case SID:
329                 principalsList.add(new RolePrincipal(group.getSidString()));
330                 break;
331             case BOTH:
332                 principalsList.add(new RolePrincipal(group.getFqn()));
333                 principalsList.add(new RolePrincipal(group.getSidString()));
334                 break;
335             case NONE:
336                 break;
337             default:
338                 break;
339         }
340         return principalsList;
341     }
342 
343     /**
344      * True if Guest login permitted.
345      *
346      * @return True if Guest login permitted, false otherwise.
347      */
348     public boolean isAllowGuestLogin() {
349         return this.allowGuestLogin;
350     }
351 
352     /**
353      * Set whether Guest login is permitted. Default is true, if the Guest account is enabled, an invalid
354      * username/password results in a Guest login.
355      *
356      * @param value
357      *            True or false.
358      */
359     public void setAllowGuestLogin(final boolean value) {
360         this.allowGuestLogin = value;
361     }
362 }