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.http.HttpServletResponse;
20  import javax.servlet.http.HttpSession;
21  
22  import org.apache.catalina.LifecycleException;
23  import org.apache.catalina.connector.Request;
24  import org.slf4j.LoggerFactory;
25  
26  import com.google.common.io.BaseEncoding;
27  import com.sun.jna.platform.win32.Win32Exception;
28  
29  import waffle.util.AuthorizationHeader;
30  import waffle.util.NtlmServletRequest;
31  import waffle.windows.auth.IWindowsIdentity;
32  import waffle.windows.auth.IWindowsSecurityContext;
33  
34  /**
35   * An Apache Negotiate (NTLM, Kerberos) Authenticator.
36   * 
37   * @author dblock[at]dblock[dot]org
38   */
39  public class NegotiateAuthenticator extends WaffleAuthenticatorBase {
40  
41      /**
42       * Instantiates a new negotiate authenticator.
43       */
44      public NegotiateAuthenticator() {
45          super();
46          this.log = LoggerFactory.getLogger(NegotiateAuthenticator.class);
47          this.info = "waffle.apache.NegotiateAuthenticator/1.0";
48          this.log.debug("[waffle.apache.NegotiateAuthenticator] loaded");
49      }
50  
51      /* (non-Javadoc)
52       * @see org.apache.catalina.authenticator.AuthenticatorBase#startInternal()
53       */
54      @Override
55      public synchronized void startInternal() throws LifecycleException {
56          this.log.info("[waffle.apache.NegotiateAuthenticator] started");
57          super.startInternal();
58      }
59  
60      /* (non-Javadoc)
61       * @see org.apache.catalina.authenticator.AuthenticatorBase#stopInternal()
62       */
63      @Override
64      public synchronized void stopInternal() throws LifecycleException {
65          super.stopInternal();
66          this.log.info("[waffle.apache.NegotiateAuthenticator] stopped");
67      }
68  
69      /* (non-Javadoc)
70       * @see org.apache.catalina.authenticator.AuthenticatorBase#authenticate(org.apache.catalina.connector.Request, javax.servlet.http.HttpServletResponse)
71       */
72      @Override
73      public boolean authenticate(final Request request, final HttpServletResponse response) {
74  
75          Principal principal = request.getUserPrincipal();
76          final AuthorizationHeader authorizationHeader = new AuthorizationHeader(request);
77          final boolean ntlmPost = authorizationHeader.isNtlmType1PostAuthorizationHeader();
78  
79          this.log.debug("{} {}, contentlength: {}", request.getMethod(), request.getRequestURI(),
80                  Integer.valueOf(request.getContentLength()));
81          this.log.debug("authorization: {}, ntlm post: {}", authorizationHeader, Boolean.valueOf(ntlmPost));
82  
83          if (principal != null && !ntlmPost) {
84              // user already authenticated
85              this.log.debug("previously authenticated user: {}", principal.getName());
86              return true;
87          }
88  
89          // authenticate user
90          if (!authorizationHeader.isNull()) {
91  
92              final String securityPackage = authorizationHeader.getSecurityPackage();
93              // maintain a connection-based session for NTLM tokens
94              final String connectionId = NtlmServletRequest.getConnectionId(request);
95  
96              this.log.debug("security package: {}, connection id: {}", securityPackage, connectionId);
97  
98              if (ntlmPost) {
99                  // type 1 NTLM authentication message received
100                 this.auth.resetSecurityToken(connectionId);
101             }
102 
103             // log the user in using the token
104             IWindowsSecurityContext securityContext;
105 
106             try {
107                 final byte[] tokenBuffer = authorizationHeader.getTokenBytes();
108                 this.log.debug("token buffer: {} byte(s)", Integer.valueOf(tokenBuffer.length));
109                 try {
110                     securityContext = this.auth.acceptSecurityToken(connectionId, tokenBuffer, securityPackage);
111                 } catch (final Win32Exception e) {
112                     this.log.warn("error logging in user: {}", e.getMessage());
113                     this.log.trace("{}", e);
114                     this.sendUnauthorized(response);
115                     return false;
116                 }
117                 this.log.debug("continue required: {}", Boolean.valueOf(securityContext.isContinue()));
118 
119                 final byte[] continueTokenBytes = securityContext.getToken();
120                 if (continueTokenBytes != null && continueTokenBytes.length > 0) {
121                     final String continueToken = BaseEncoding.base64().encode(continueTokenBytes);
122                     this.log.debug("continue token: {}", continueToken);
123                     response.addHeader("WWW-Authenticate", securityPackage + " " + continueToken);
124                 }
125 
126                 if (securityContext.isContinue() || ntlmPost) {
127                     response.setHeader("Connection", "keep-alive");
128                     response.sendError(HttpServletResponse.SC_UNAUTHORIZED);
129                     response.flushBuffer();
130                     return false;
131                 }
132 
133             } catch (final IOException e) {
134                 this.log.warn("error logging in user: {}", e.getMessage());
135                 this.log.trace("{}", e);
136                 this.sendUnauthorized(response);
137                 return false;
138             }
139 
140             // realm: fail if no realm is configured
141             if (this.context == null || this.context.getRealm() == null) {
142                 this.log.warn("missing context/realm");
143                 this.sendError(response, HttpServletResponse.SC_SERVICE_UNAVAILABLE);
144                 return false;
145             }
146 
147             // create and register the user principal with the session
148             final IWindowsIdentity windowsIdentity = securityContext.getIdentity();
149 
150             // disable guest login
151             if (!this.allowGuestLogin && windowsIdentity.isGuest()) {
152                 this.log.warn("guest login disabled: {}", windowsIdentity.getFqn());
153                 this.sendUnauthorized(response);
154                 return false;
155             }
156 
157             try {
158                 this.log.debug("logged in user: {} ({})", windowsIdentity.getFqn(), windowsIdentity.getSidString());
159 
160                 final GenericWindowsPrincipal windowsPrincipal = new GenericWindowsPrincipal(windowsIdentity,
161                         this.principalFormat, this.roleFormat);
162 
163                 this.log.debug("roles: {}", windowsPrincipal.getRolesString());
164 
165                 principal = windowsPrincipal;
166 
167                 // create a session associated with this request if there's none
168                 final HttpSession session = request.getSession(true);
169                 this.log.debug("session id: {}", session == null ? "null" : session.getId());
170 
171                 // register the authenticated principal
172                 this.register(request, response, principal, securityPackage, principal.getName(), null);
173                 this.log.info("successfully logged in user: {}", principal.getName());
174 
175             } finally {
176                 windowsIdentity.dispose();
177             }
178 
179             return true;
180         }
181 
182         this.log.debug("authorization required");
183         this.sendUnauthorized(response);
184         return false;
185     }
186 }