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