View Javadoc
1   /**
2    * Waffle (https://github.com/dblock/waffle)
3    *
4    * Copyright (c) 2010 - 2015 Application Security, Inc.
5    *
6    * All rights reserved. This program and the accompanying materials
7    * are made available under the terms of the Eclipse Public License v1.0
8    * which accompanies this distribution, and is available at
9    * http://www.eclipse.org/legal/epl-v10.html
10   *
11   * Contributors:
12   *     Application Security, Inc.
13   */
14  package waffle.apache;
15  
16  import java.io.IOException;
17  import java.security.Principal;
18  
19  import javax.servlet.RequestDispatcher;
20  import javax.servlet.ServletContext;
21  import javax.servlet.ServletException;
22  import javax.servlet.http.HttpServletResponse;
23  import javax.servlet.http.HttpSession;
24  
25  import org.apache.catalina.LifecycleException;
26  import org.apache.catalina.connector.Request;
27  import org.apache.tomcat.util.descriptor.web.LoginConfig;
28  import org.slf4j.LoggerFactory;
29  
30  import com.google.common.io.BaseEncoding;
31  
32  import waffle.util.AuthorizationHeader;
33  import waffle.util.NtlmServletRequest;
34  import waffle.windows.auth.IWindowsIdentity;
35  import waffle.windows.auth.IWindowsSecurityContext;
36  
37  /**
38   * Mixed Negotiate + Form Authenticator.
39   * 
40   * @author dblock[at]dblock[dot]org
41   */
42  public class MixedAuthenticator extends WaffleAuthenticatorBase {
43  
44      /**
45       * Instantiates a new mixed authenticator.
46       */
47      public MixedAuthenticator() {
48          super();
49          this.log = LoggerFactory.getLogger(MixedAuthenticator.class);
50          this.info = "waffle.apache.MixedAuthenticator/1.0";
51          this.log.debug("[waffle.apache.MixedAuthenticator] loaded");
52      }
53  
54      /* (non-Javadoc)
55       * @see org.apache.catalina.authenticator.AuthenticatorBase#startInternal()
56       */
57      @Override
58      public synchronized void startInternal() throws LifecycleException {
59          this.log.info("[waffle.apache.MixedAuthenticator] started");
60          super.startInternal();
61      }
62  
63      /* (non-Javadoc)
64       * @see org.apache.catalina.authenticator.AuthenticatorBase#stopInternal()
65       */
66      @Override
67      public synchronized void stopInternal() throws LifecycleException {
68          super.stopInternal();
69          this.log.info("[waffle.apache.MixedAuthenticator] stopped");
70      }
71  
72      /* (non-Javadoc)
73       * @see org.apache.catalina.authenticator.AuthenticatorBase#authenticate(org.apache.catalina.connector.Request, javax.servlet.http.HttpServletResponse)
74       */
75      @Override
76      public boolean authenticate(final Request request, final HttpServletResponse response) {
77  
78          // realm: fail if no realm is configured
79          if (this.context == null || this.context.getRealm() == null) {
80              this.log.warn("missing context/realm");
81              this.sendError(response, HttpServletResponse.SC_SERVICE_UNAVAILABLE);
82              return false;
83          }
84  
85          this.log.debug("{} {}, contentlength: {}", request.getMethod(), request.getRequestURI(),
86                  Integer.valueOf(request.getContentLength()));
87  
88          final boolean negotiateCheck = request.getParameter("j_negotiate_check") != null;
89          this.log.debug("negotiateCheck: {}", Boolean.valueOf(negotiateCheck));
90          final boolean securityCheck = request.getParameter("j_security_check") != null;
91          this.log.debug("securityCheck: {}", Boolean.valueOf(securityCheck));
92  
93          final Principal principal = request.getUserPrincipal();
94  
95          final AuthorizationHeader authorizationHeader = new AuthorizationHeader(request);
96          final boolean ntlmPost = authorizationHeader.isNtlmType1PostAuthorizationHeader();
97          this.log.debug("authorization: {}, ntlm post: {}", authorizationHeader, Boolean.valueOf(ntlmPost));
98  
99          final LoginConfig loginConfig = this.context.getLoginConfig();
100 
101         if (principal != null && !ntlmPost) {
102             this.log.debug("previously authenticated user: {}", principal.getName());
103             return true;
104         } else if (negotiateCheck) {
105             if (!authorizationHeader.isNull()) {
106                 return this.negotiate(request, response, authorizationHeader);
107             }
108             this.log.debug("authorization required");
109             this.sendUnauthorized(response);
110             return false;
111         } else if (securityCheck) {
112             final boolean postResult = this.post(request, response);
113             if (postResult) {
114                 this.redirectTo(request, response, request.getServletPath());
115             } else {
116                 this.redirectTo(request, response, loginConfig.getErrorPage());
117             }
118             return postResult;
119         } else {
120             this.redirectTo(request, response, loginConfig.getLoginPage());
121             return false;
122         }
123     }
124 
125     /**
126      * Negotiate.
127      *
128      * @param request
129      *            the request
130      * @param response
131      *            the response
132      * @param authorizationHeader
133      *            the authorization header
134      * @return true, if successful
135      */
136     private boolean negotiate(final Request request, final HttpServletResponse response,
137             final AuthorizationHeader authorizationHeader) {
138 
139         final String securityPackage = authorizationHeader.getSecurityPackage();
140         // maintain a connection-based session for NTLM tokens
141         final String connectionId = NtlmServletRequest.getConnectionId(request);
142 
143         this.log.debug("security package: {}, connection id: {}", securityPackage, connectionId);
144 
145         final boolean ntlmPost = authorizationHeader.isNtlmType1PostAuthorizationHeader();
146 
147         if (ntlmPost) {
148             // type 1 NTLM authentication message received
149             this.auth.resetSecurityToken(connectionId);
150         }
151 
152         // log the user in using the token
153         IWindowsSecurityContext securityContext;
154 
155         try {
156             final byte[] tokenBuffer = authorizationHeader.getTokenBytes();
157             this.log.debug("token buffer: {} byte(s)", Integer.valueOf(tokenBuffer.length));
158             securityContext = this.auth.acceptSecurityToken(connectionId, tokenBuffer, securityPackage);
159             this.log.debug("continue required: {}", Boolean.valueOf(securityContext.isContinue()));
160 
161             final byte[] continueTokenBytes = securityContext.getToken();
162             if (continueTokenBytes != null && continueTokenBytes.length > 0) {
163                 final String continueToken = BaseEncoding.base64().encode(continueTokenBytes);
164                 this.log.debug("continue token: {}", continueToken);
165                 response.addHeader("WWW-Authenticate", securityPackage + " " + continueToken);
166             }
167 
168             if (securityContext.isContinue() || ntlmPost) {
169                 response.setHeader("Connection", "keep-alive");
170                 response.sendError(HttpServletResponse.SC_UNAUTHORIZED);
171                 response.flushBuffer();
172                 return false;
173             }
174 
175         } catch (final IOException e) {
176             this.log.warn("error logging in user: {}", e.getMessage());
177             this.log.trace("{}", e);
178             this.sendUnauthorized(response);
179             return false;
180         }
181 
182         // create and register the user principal with the session
183         final IWindowsIdentity windowsIdentity = securityContext.getIdentity();
184 
185         // disable guest login
186         if (!this.allowGuestLogin && windowsIdentity.isGuest()) {
187             this.log.warn("guest login disabled: {}", windowsIdentity.getFqn());
188             this.sendUnauthorized(response);
189             return false;
190         }
191 
192         try {
193 
194             this.log.debug("logged in user: {} ({})", windowsIdentity.getFqn(), windowsIdentity.getSidString());
195 
196             final GenericWindowsPrincipal windowsPrincipal = new GenericWindowsPrincipal(windowsIdentity,
197                     this.principalFormat, this.roleFormat);
198 
199             this.log.debug("roles: {}", windowsPrincipal.getRolesString());
200 
201             // create a session associated with this request if there's none
202             final HttpSession session = request.getSession(true);
203             this.log.debug("session id: {}", session == null ? "null" : session.getId());
204 
205             this.register(request, response, windowsPrincipal, securityPackage, windowsPrincipal.getName(), null);
206             this.log.info("successfully logged in user: {}", windowsPrincipal.getName());
207 
208         } finally {
209             windowsIdentity.dispose();
210         }
211 
212         return true;
213     }
214 
215     /**
216      * Post.
217      *
218      * @param request
219      *            the request
220      * @param response
221      *            the response
222      * @return true, if successful
223      */
224     private boolean post(final Request request, final HttpServletResponse response) {
225 
226         final String username = request.getParameter("j_username");
227         final String password = request.getParameter("j_password");
228 
229         this.log.debug("logging in: {}", username);
230 
231         IWindowsIdentity windowsIdentity;
232         try {
233             windowsIdentity = this.auth.logonUser(username, password);
234         } catch (final Exception e) {
235             this.log.error(e.getMessage());
236             this.log.trace("{}", e);
237             return false;
238         }
239 
240         // disable guest login
241         if (!this.allowGuestLogin && windowsIdentity.isGuest()) {
242             this.log.warn("guest login disabled: {}", windowsIdentity.getFqn());
243             return false;
244         }
245 
246         try {
247             this.log.debug("successfully logged in {} ({})", username, windowsIdentity.getSidString());
248 
249             final GenericWindowsPrincipal windowsPrincipal = new GenericWindowsPrincipal(windowsIdentity,
250                     this.principalFormat, this.roleFormat);
251 
252             this.log.debug("roles: {}", windowsPrincipal.getRolesString());
253 
254             // create a session associated with this request if there's none
255             final HttpSession session = request.getSession(true);
256             this.log.debug("session id: {}", session == null ? "null" : session.getId());
257 
258             this.register(request, response, windowsPrincipal, "FORM", windowsPrincipal.getName(), null);
259             this.log.info("successfully logged in user: {}", windowsPrincipal.getName());
260         } finally {
261             windowsIdentity.dispose();
262         }
263 
264         return true;
265     }
266 
267     /**
268      * Redirect to.
269      *
270      * @param request
271      *            the request
272      * @param response
273      *            the response
274      * @param url
275      *            the url
276      */
277     private void redirectTo(final Request request, final HttpServletResponse response, final String url) {
278         try {
279             this.log.debug("redirecting to: {}", url);
280             final ServletContext servletContext = this.context.getServletContext();
281             final RequestDispatcher disp = servletContext.getRequestDispatcher(url);
282             disp.forward(request.getRequest(), response);
283         } catch (final IOException e) {
284             this.log.error(e.getMessage());
285             this.log.trace("{}", e);
286             throw new RuntimeException(e);
287         } catch (final ServletException e) {
288             this.log.error(e.getMessage());
289             this.log.trace("{}", e);
290             throw new RuntimeException(e);
291         }
292     }
293 }