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.http.HttpServletResponse;
33  import javax.servlet.http.HttpSession;
34  
35  import org.apache.catalina.LifecycleException;
36  import org.apache.catalina.connector.Request;
37  import org.apache.catalina.realm.GenericPrincipal;
38  import org.slf4j.LoggerFactory;
39  
40  import waffle.util.AuthorizationHeader;
41  import waffle.util.NtlmServletRequest;
42  import waffle.windows.auth.IWindowsIdentity;
43  import waffle.windows.auth.IWindowsSecurityContext;
44  
45  /**
46   * An Apache Negotiate (NTLM, Kerberos) Authenticator.
47   */
48  public class NegotiateAuthenticator extends WaffleAuthenticatorBase {
49  
50      /**
51       * Instantiates a new negotiate authenticator.
52       */
53      public NegotiateAuthenticator() {
54          super();
55          this.log = LoggerFactory.getLogger(NegotiateAuthenticator.class);
56          this.info = "waffle.apache.NegotiateAuthenticator/1.0";
57          this.log.debug("[waffle.apache.NegotiateAuthenticator] loaded");
58      }
59  
60      @Override
61      public synchronized void startInternal() throws LifecycleException {
62          this.log.info("[waffle.apache.NegotiateAuthenticator] started");
63          super.startInternal();
64      }
65  
66      @Override
67      public synchronized void stopInternal() throws LifecycleException {
68          super.stopInternal();
69          this.log.info("[waffle.apache.NegotiateAuthenticator] stopped");
70      }
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             final byte[] tokenBuffer = authorizationHeader.getTokenBytes();
104             this.log.debug("token buffer: {} byte(s)", Integer.valueOf(tokenBuffer.length));
105 
106             // log the user in using the token
107             IWindowsSecurityContext securityContext;
108             try {
109                 securityContext = this.auth.acceptSecurityToken(connectionId, tokenBuffer, securityPackage);
110             } catch (final Win32Exception e) {
111                 this.log.warn("error logging in user: {}", e.getMessage());
112                 this.log.trace("", e);
113                 this.sendUnauthorized(response);
114                 return false;
115             }
116             this.log.debug("continue required: {}", Boolean.valueOf(securityContext.isContinue()));
117 
118             final byte[] continueTokenBytes = securityContext.getToken();
119             if (continueTokenBytes != null && continueTokenBytes.length > 0) {
120                 final String continueToken = Base64.getEncoder().encodeToString(continueTokenBytes);
121                 this.log.debug("continue token: {}", continueToken);
122                 response.addHeader("WWW-Authenticate", securityPackage + " " + continueToken);
123             }
124 
125             try {
126                 if (securityContext.isContinue()) {
127                     response.setHeader("Connection", "keep-alive");
128                     response.sendError(HttpServletResponse.SC_UNAUTHORIZED);
129                     response.flushBuffer();
130                     return false;
131                 }
132             } catch (final IOException e) {
133                 this.log.warn("error logging in user: {}", e.getMessage());
134                 this.log.trace("", e);
135                 this.sendUnauthorized(response);
136                 return false;
137             }
138 
139             // realm: fail if no realm is configured
140             if (this.context == null || this.context.getRealm() == null) {
141                 this.log.warn("missing context/realm");
142                 this.sendError(response, HttpServletResponse.SC_SERVICE_UNAVAILABLE);
143                 return false;
144             }
145 
146             // create and register the user principal with the session
147             final IWindowsIdentity windowsIdentity = securityContext.getIdentity();
148 
149             // disable guest login
150             if (!this.allowGuestLogin && windowsIdentity.isGuest()) {
151                 this.log.warn("guest login disabled: {}", windowsIdentity.getFqn());
152                 this.sendUnauthorized(response);
153                 return false;
154             }
155 
156             try {
157                 this.log.debug("logged in user: {} ({})", windowsIdentity.getFqn(), windowsIdentity.getSidString());
158 
159                 final GenericPrincipal genericPrincipal = this.createPrincipal(windowsIdentity);
160 
161                 if (this.log.isDebugEnabled()) {
162                     this.log.debug("roles: {}", String.join(", ", genericPrincipal.getRoles()));
163                 }
164 
165                 principal = genericPrincipal;
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                 securityContext.dispose();
178             }
179 
180             return true;
181         }
182 
183         this.log.debug("authorization required");
184         this.sendUnauthorized(response);
185         return false;
186     }
187 
188     /**
189      * XXX The 'doAuthenticate' is intended to replace 'authenticate' for needs like ours. In order to support old and
190      * new at this time, we will continue to have both for time being.
191      */
192     @Override
193     protected boolean doAuthenticate(final Request request, final HttpServletResponse response) throws IOException {
194         return this.authenticate(request, response);
195     }
196 
197 }