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.catalina.deploy.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, org.apache.catalina.deploy.LoginConfig)
74       */
75      @Override
76      public boolean authenticate(final Request request, final HttpServletResponse response, final LoginConfig loginConfig) {
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          if (principal != null && !ntlmPost) {
100             this.log.debug("previously authenticated user: {}", principal.getName());
101             return true;
102         } else if (negotiateCheck) {
103             if (!authorizationHeader.isNull()) {
104                 return this.negotiate(request, response, authorizationHeader);
105             }
106             this.log.debug("authorization required");
107             this.sendUnauthorized(response);
108             return false;
109         } else if (securityCheck) {
110             final boolean postResult = this.post(request, response);
111             if (postResult) {
112                 this.redirectTo(request, response, request.getServletPath());
113             } else {
114                 this.redirectTo(request, response, loginConfig.getErrorPage());
115             }
116             return postResult;
117         } else {
118             this.redirectTo(request, response, loginConfig.getLoginPage());
119             return false;
120         }
121     }
122 
123     /**
124      * Negotiate.
125      *
126      * @param request
127      *            the request
128      * @param response
129      *            the response
130      * @param authorizationHeader
131      *            the authorization header
132      * @return true, if successful
133      */
134     private boolean negotiate(final Request request, final HttpServletResponse response,
135             final AuthorizationHeader authorizationHeader) {
136 
137         final String securityPackage = authorizationHeader.getSecurityPackage();
138         // maintain a connection-based session for NTLM tokens
139         final String connectionId = NtlmServletRequest.getConnectionId(request);
140 
141         this.log.debug("security package: {}, connection id: {}", securityPackage, connectionId);
142 
143         final boolean ntlmPost = authorizationHeader.isNtlmType1PostAuthorizationHeader();
144 
145         if (ntlmPost) {
146             // type 1 NTLM authentication message received
147             this.auth.resetSecurityToken(connectionId);
148         }
149 
150         // log the user in using the token
151         IWindowsSecurityContext securityContext;
152 
153         try {
154             final byte[] tokenBuffer = authorizationHeader.getTokenBytes();
155             this.log.debug("token buffer: {} byte(s)", Integer.valueOf(tokenBuffer.length));
156             securityContext = this.auth.acceptSecurityToken(connectionId, tokenBuffer, securityPackage);
157             this.log.debug("continue required: {}", Boolean.valueOf(securityContext.isContinue()));
158 
159             final byte[] continueTokenBytes = securityContext.getToken();
160             if (continueTokenBytes != null && continueTokenBytes.length > 0) {
161                 final String continueToken = BaseEncoding.base64().encode(continueTokenBytes);
162                 this.log.debug("continue token: {}", continueToken);
163                 response.addHeader("WWW-Authenticate", securityPackage + " " + continueToken);
164             }
165 
166             if (securityContext.isContinue() || ntlmPost) {
167                 response.setHeader("Connection", "keep-alive");
168                 response.sendError(HttpServletResponse.SC_UNAUTHORIZED);
169                 response.flushBuffer();
170                 return false;
171             }
172 
173         } catch (final IOException e) {
174             this.log.warn("error logging in user: {}", e.getMessage());
175             this.log.trace("{}", e);
176             this.sendUnauthorized(response);
177             return false;
178         }
179 
180         // create and register the user principal with the session
181         final IWindowsIdentity windowsIdentity = securityContext.getIdentity();
182 
183         // disable guest login
184         if (!this.allowGuestLogin && windowsIdentity.isGuest()) {
185             this.log.warn("guest login disabled: {}", windowsIdentity.getFqn());
186             this.sendUnauthorized(response);
187             return false;
188         }
189 
190         try {
191 
192             this.log.debug("logged in user: {} ({})", windowsIdentity.getFqn(), windowsIdentity.getSidString());
193 
194             final GenericWindowsPrincipal windowsPrincipal = new GenericWindowsPrincipal(windowsIdentity,
195                     this.principalFormat, this.roleFormat);
196 
197             this.log.debug("roles: {}", windowsPrincipal.getRolesString());
198 
199             // create a session associated with this request if there's none
200             final HttpSession session = request.getSession(true);
201             this.log.debug("session id: {}", session == null ? "null" : session.getId());
202 
203             this.register(request, response, windowsPrincipal, securityPackage, windowsPrincipal.getName(), null);
204             this.log.info("successfully logged in user: {}", windowsPrincipal.getName());
205 
206         } finally {
207             windowsIdentity.dispose();
208         }
209 
210         return true;
211     }
212 
213     /**
214      * Post.
215      *
216      * @param request
217      *            the request
218      * @param response
219      *            the response
220      * @return true, if successful
221      */
222     private boolean post(final Request request, final HttpServletResponse response) {
223 
224         final String username = request.getParameter("j_username");
225         final String password = request.getParameter("j_password");
226 
227         this.log.debug("logging in: {}", username);
228 
229         IWindowsIdentity windowsIdentity;
230         try {
231             windowsIdentity = this.auth.logonUser(username, password);
232         } catch (final Exception e) {
233             this.log.error(e.getMessage());
234             this.log.trace("{}", e);
235             return false;
236         }
237 
238         // disable guest login
239         if (!this.allowGuestLogin && windowsIdentity.isGuest()) {
240             this.log.warn("guest login disabled: {}", windowsIdentity.getFqn());
241             return false;
242         }
243 
244         try {
245             this.log.debug("successfully logged in {} ({})", username, windowsIdentity.getSidString());
246 
247             final GenericWindowsPrincipal windowsPrincipal = new GenericWindowsPrincipal(windowsIdentity,
248                     this.principalFormat, this.roleFormat);
249 
250             this.log.debug("roles: {}", windowsPrincipal.getRolesString());
251 
252             // create a session associated with this request if there's none
253             final HttpSession session = request.getSession(true);
254             this.log.debug("session id: {}", session == null ? "null" : session.getId());
255 
256             this.register(request, response, windowsPrincipal, "FORM", windowsPrincipal.getName(), null);
257             this.log.info("successfully logged in user: {}", windowsPrincipal.getName());
258         } finally {
259             windowsIdentity.dispose();
260         }
261 
262         return true;
263     }
264 
265     /**
266      * Redirect to.
267      *
268      * @param request
269      *            the request
270      * @param response
271      *            the response
272      * @param url
273      *            the url
274      */
275     private void redirectTo(final Request request, final HttpServletResponse response, final String url) {
276         try {
277             this.log.debug("redirecting to: {}", url);
278             final ServletContext servletContext = this.context.getServletContext();
279             final RequestDispatcher disp = servletContext.getRequestDispatcher(url);
280             disp.forward(request.getRequest(), response);
281         } catch (final IOException e) {
282             this.log.error(e.getMessage());
283             this.log.trace("{}", e);
284             throw new RuntimeException(e);
285         } catch (final ServletException e) {
286             this.log.error(e.getMessage());
287             this.log.trace("{}", e);
288             throw new RuntimeException(e);
289         }
290     }
291 }