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.apache;
25  
26  import com.sun.jna.platform.win32.Win32Exception;
27  
28  import java.io.IOException;
29  import java.security.Principal;
30  import java.util.Base64;
31  
32  import javax.servlet.RequestDispatcher;
33  import javax.servlet.ServletContext;
34  import javax.servlet.ServletException;
35  import javax.servlet.http.HttpServletResponse;
36  import javax.servlet.http.HttpSession;
37  
38  import org.apache.catalina.LifecycleException;
39  import org.apache.catalina.connector.Request;
40  import org.apache.catalina.realm.GenericPrincipal;
41  import org.apache.tomcat.util.descriptor.web.LoginConfig;
42  import org.slf4j.LoggerFactory;
43  
44  import waffle.util.AuthorizationHeader;
45  import waffle.util.NtlmServletRequest;
46  import waffle.windows.auth.IWindowsIdentity;
47  import waffle.windows.auth.IWindowsSecurityContext;
48  
49  /**
50   * Mixed Negotiate + Form Authenticator.
51   */
52  public class MixedAuthenticator extends WaffleAuthenticatorBase {
53  
54      /**
55       * Instantiates a new mixed authenticator.
56       */
57      public MixedAuthenticator() {
58          super();
59          this.log = LoggerFactory.getLogger(MixedAuthenticator.class);
60          this.info = "waffle.apache.MixedAuthenticator/1.0";
61          this.log.debug("[waffle.apache.MixedAuthenticator] loaded");
62      }
63  
64      @Override
65      public synchronized void startInternal() throws LifecycleException {
66          this.log.info("[waffle.apache.MixedAuthenticator] started");
67          super.startInternal();
68      }
69  
70      @Override
71      public synchronized void stopInternal() throws LifecycleException {
72          super.stopInternal();
73          this.log.info("[waffle.apache.MixedAuthenticator] stopped");
74      }
75  
76      @Override
77      public boolean authenticate(final Request request, final HttpServletResponse response) {
78  
79          // realm: fail if no realm is configured
80          if (this.context == null || this.context.getRealm() == null) {
81              this.log.warn("missing context/realm");
82              this.sendError(response, HttpServletResponse.SC_SERVICE_UNAVAILABLE);
83              return false;
84          }
85  
86          this.log.debug("{} {}, contentlength: {}", request.getMethod(), request.getRequestURI(),
87                  Integer.valueOf(request.getContentLength()));
88  
89          final boolean negotiateCheck = request.getParameter("j_negotiate_check") != null;
90          this.log.debug("negotiateCheck: {}", Boolean.valueOf(negotiateCheck));
91          final boolean securityCheck = request.getParameter("j_security_check") != null;
92          this.log.debug("securityCheck: {}", Boolean.valueOf(securityCheck));
93  
94          final Principal principal = request.getUserPrincipal();
95  
96          final AuthorizationHeader authorizationHeader = new AuthorizationHeader(request);
97          final boolean ntlmPost = authorizationHeader.isNtlmType1PostAuthorizationHeader();
98          this.log.debug("authorization: {}, ntlm post: {}", authorizationHeader, Boolean.valueOf(ntlmPost));
99  
100         final LoginConfig loginConfig = this.context.getLoginConfig();
101 
102         if (principal != null && !ntlmPost) {
103             this.log.debug("previously authenticated user: {}", principal.getName());
104             return true;
105         } else if (negotiateCheck) {
106             if (!authorizationHeader.isNull()) {
107                 boolean negotiateResult = this.negotiate(request, response, authorizationHeader);
108                 if (!negotiateResult) {
109                     this.redirectTo(request, response, loginConfig.getErrorPage());
110                 }
111                 return negotiateResult;
112             }
113             this.log.debug("authorization required");
114             this.sendUnauthorized(response);
115             return false;
116         } else if (securityCheck) {
117             final boolean postResult = this.post(request, response);
118             if (!postResult) {
119                 this.redirectTo(request, response, loginConfig.getErrorPage());
120             }
121             return postResult;
122         } else {
123             this.redirectTo(request, response, loginConfig.getLoginPage());
124             return false;
125         }
126     }
127 
128     /**
129      * Negotiate.
130      *
131      * @param request
132      *            the request
133      * @param response
134      *            the response
135      * @param authorizationHeader
136      *            the authorization header
137      *
138      * @return true, if successful
139      */
140     private boolean negotiate(final Request request, final HttpServletResponse response,
141             final AuthorizationHeader authorizationHeader) {
142 
143         final String securityPackage = authorizationHeader.getSecurityPackage();
144         // maintain a connection-based session for NTLM tokens
145         final String connectionId = NtlmServletRequest.getConnectionId(request);
146 
147         this.log.debug("security package: {}, connection id: {}", securityPackage, connectionId);
148 
149         final boolean ntlmPost = authorizationHeader.isNtlmType1PostAuthorizationHeader();
150 
151         if (ntlmPost) {
152             // type 1 NTLM authentication message received
153             this.auth.resetSecurityToken(connectionId);
154         }
155 
156         final byte[] tokenBuffer = authorizationHeader.getTokenBytes();
157         this.log.debug("token buffer: {} byte(s)", Integer.valueOf(tokenBuffer.length));
158 
159         // log the user in using the token
160         IWindowsSecurityContext securityContext;
161         try {
162             securityContext = this.auth.acceptSecurityToken(connectionId, tokenBuffer, securityPackage);
163         } catch (final Win32Exception e) {
164             this.log.warn("error logging in user: {}", e.getMessage());
165             this.log.trace("", e);
166             this.sendUnauthorized(response);
167             return false;
168         }
169         this.log.debug("continue required: {}", Boolean.valueOf(securityContext.isContinue()));
170 
171         final byte[] continueTokenBytes = securityContext.getToken();
172         if (continueTokenBytes != null && continueTokenBytes.length > 0) {
173             final String continueToken = Base64.getEncoder().encodeToString(continueTokenBytes);
174             this.log.debug("continue token: {}", continueToken);
175             response.addHeader("WWW-Authenticate", securityPackage + " " + continueToken);
176         }
177 
178         try {
179             if (securityContext.isContinue() || ntlmPost) {
180                 response.setHeader("Connection", "keep-alive");
181                 response.sendError(HttpServletResponse.SC_UNAUTHORIZED);
182                 response.flushBuffer();
183                 return false;
184             }
185         } catch (final IOException e) {
186             this.log.warn("error logging in user: {}", e.getMessage());
187             this.log.trace("", e);
188             this.sendUnauthorized(response);
189             return false;
190         }
191 
192         // create and register the user principal with the session
193         final IWindowsIdentity windowsIdentity = securityContext.getIdentity();
194 
195         // disable guest login
196         if (!this.allowGuestLogin && windowsIdentity.isGuest()) {
197             this.log.warn("guest login disabled: {}", windowsIdentity.getFqn());
198             this.sendUnauthorized(response);
199             return false;
200         }
201 
202         try {
203 
204             this.log.debug("logged in user: {} ({})", windowsIdentity.getFqn(), windowsIdentity.getSidString());
205 
206             final GenericPrincipal genericPrincipal = this.createPrincipal(windowsIdentity);
207 
208             if (this.log.isDebugEnabled()) {
209                 this.log.debug("roles: {}", String.join(", ", genericPrincipal.getRoles()));
210             }
211 
212             // create a session associated with this request if there's none
213             final HttpSession session = request.getSession(true);
214             this.log.debug("session id: {}", session == null ? "null" : session.getId());
215 
216             this.register(request, response, genericPrincipal, securityPackage, genericPrincipal.getName(), null);
217             this.log.info("successfully logged in user: {}", genericPrincipal.getName());
218 
219         } finally {
220             windowsIdentity.dispose();
221         }
222 
223         return true;
224     }
225 
226     /**
227      * Post.
228      *
229      * @param request
230      *            the request
231      * @param response
232      *            the response
233      *
234      * @return true, if successful
235      */
236     private boolean post(final Request request, final HttpServletResponse response) {
237 
238         final String username = request.getParameter("j_username");
239         final String password = request.getParameter("j_password");
240 
241         this.log.debug("logging in: {}", username);
242 
243         IWindowsIdentity windowsIdentity;
244         try {
245             windowsIdentity = this.auth.logonUser(username, password);
246         } catch (final Exception e) {
247             this.log.error(e.getMessage());
248             this.log.trace("", e);
249             return false;
250         }
251 
252         // disable guest login
253         if (!this.allowGuestLogin && windowsIdentity.isGuest()) {
254             this.log.warn("guest login disabled: {}", windowsIdentity.getFqn());
255             return false;
256         }
257 
258         try {
259             this.log.debug("successfully logged in {} ({})", username, windowsIdentity.getSidString());
260 
261             final GenericPrincipal genericPrincipal = this.createPrincipal(windowsIdentity);
262 
263             if (this.log.isDebugEnabled()) {
264                 this.log.debug("roles: {}", String.join(", ", genericPrincipal.getRoles()));
265             }
266 
267             // create a session associated with this request if there's none
268             final HttpSession session = request.getSession(true);
269             this.log.debug("session id: {}", session == null ? "null" : session.getId());
270 
271             this.register(request, response, genericPrincipal, "FORM", genericPrincipal.getName(), null);
272             this.log.info("successfully logged in user: {}", genericPrincipal.getName());
273         } finally {
274             windowsIdentity.dispose();
275         }
276 
277         return true;
278     }
279 
280     /**
281      * Redirect to.
282      *
283      * @param request
284      *            the request
285      * @param response
286      *            the response
287      * @param url
288      *            the url
289      */
290     private void redirectTo(final Request request, final HttpServletResponse response, final String url) {
291         try {
292             this.log.debug("redirecting to: {}", url);
293             final ServletContext servletContext = this.context.getServletContext();
294             final RequestDispatcher disp = servletContext.getRequestDispatcher(url);
295             disp.forward(request.getRequest(), response);
296         } catch (final IOException | ServletException e) {
297             throw new RuntimeException(e);
298         }
299     }
300 
301     /**
302      * XXX The 'doAuthenticate' is intended to replace 'authenticate' for needs like ours. In order to support old and
303      * new at this time, we will continue to have both for time being.
304      */
305     @Override
306     protected boolean doAuthenticate(final Request request, final HttpServletResponse response) throws IOException {
307         return this.authenticate(request, response);
308     }
309 
310 }