View Javadoc
1   /*
2    * Copyright 2013–2026 Michael Osipov
3    *
4    * Licensed under the Apache License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    *     http://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   */
16  package net.sf.michaelo.tomcat.authenticator;
17  
18  import java.io.IOException;
19  import java.security.Principal;
20  import java.security.PrivilegedActionException;
21  import java.security.PrivilegedExceptionAction;
22  import java.util.Base64;
23  
24  import javax.security.auth.Subject;
25  import javax.security.auth.login.LoginContext;
26  import javax.security.auth.login.LoginException;
27  import javax.servlet.http.HttpServletResponse;
28  
29  import net.sf.michaelo.tomcat.StringUtils;
30  
31  import org.apache.catalina.Realm;
32  import org.apache.catalina.connector.Request;
33  import org.ietf.jgss.GSSContext;
34  import org.ietf.jgss.GSSCredential;
35  import org.ietf.jgss.GSSException;
36  import org.ietf.jgss.GSSManager;
37  import org.ietf.jgss.GSSName;
38  
39  /** A SPNEGO Authenticator which utilizes GSS-API to authenticate a client. */
40  public class SpnegoAuthenticator extends GSSAuthenticatorBase {
41  
42  	protected static final String SPNEGO_METHOD = "SPNEGO";
43  	protected static final String SPNEGO_AUTH_SCHEME = "Negotiate";
44  
45  	private static final byte[] NTLM_TYPE1_MESSAGE_START = {
46  		(byte) 'N',
47  		(byte) 'T',
48  		(byte) 'L',
49  		(byte) 'M',
50  		(byte) 'S',
51  		(byte) 'S',
52  		(byte) 'P',
53  		(byte) '\0',
54  		(byte) 0x01,
55  		(byte) 0x00,
56  		(byte) 0x00,
57  		(byte) 0x00
58  	};
59  
60  	@Override
61  	protected boolean doAuthenticate(Request request, HttpServletResponse response) throws IOException {
62  
63  		/*
64  		 * Reauthentication using the cached user name and password (if any) is not enabled for SPNEGO
65  		 * authentication. This is because the delegated credential will not be available unless a normal SPNEGO
66  		 * authentication takes place.
67  		 */
68  		if (checkForCachedAuthentication(request, response, false)) {
69  			return true;
70  		}
71  
72  		String authorization = request.getHeader("Authorization");
73  
74  		if (!StringUtils.startsWithIgnoreCase(authorization, SPNEGO_AUTH_SCHEME)) {
75  			sendUnauthorized(request, response, SPNEGO_AUTH_SCHEME);
76  			return false;
77  		}
78  
79  		String authorizationValue = StringUtils.substring(authorization, SPNEGO_AUTH_SCHEME.length() + 1);
80  
81  		if (StringUtils.isEmpty(authorizationValue)) {
82  			sendUnauthorized(request, response, SPNEGO_AUTH_SCHEME);
83  			return false;
84  		}
85  
86  		byte[] outToken = null;
87  		byte[] inToken = null;
88  
89  		if (logger.isDebugEnabled())
90  			logger.debug(sm.getString("spnegoAuthenticator.processingToken", authorizationValue));
91  
92  		try {
93  			inToken = Base64.getDecoder().decode(authorizationValue);
94  		} catch (Exception e) {
95  			logger.warn(sm.getString("spnegoAuthenticator.incorrectlyEncodedToken", authorizationValue), e);
96  
97  			sendUnauthorized(
98  					request,
99  					response,
100 					SPNEGO_AUTH_SCHEME,
101 					"spnegoAuthenticator.incorrectlyEncodedToken.responseMessage");
102 			return false;
103 		}
104 
105 		if (inToken.length >= NTLM_TYPE1_MESSAGE_START.length) {
106 			boolean ntlmDetected = false;
107 			for (int i = 0; i < NTLM_TYPE1_MESSAGE_START.length; i++) {
108 				ntlmDetected = inToken[i] == NTLM_TYPE1_MESSAGE_START[i];
109 
110 				if (!ntlmDetected) break;
111 			}
112 
113 			if (ntlmDetected) {
114 				logger.warn(sm.getString("spnegoAuthenticator.ntlmNotSupported"));
115 
116 				sendUnauthorized(
117 						request, response, SPNEGO_AUTH_SCHEME, "spnegoAuthenticator.ntlmNotSupported.responseMessage");
118 				return false;
119 			}
120 		}
121 
122 		LoginContext lc = null;
123 		GSSContext gssContext = null;
124 		Principal principal = null;
125 
126 		try {
127 			try {
128 				lc = new LoginContext(getLoginEntryName());
129 				lc.login();
130 			} catch (LoginException | SecurityException e) {
131 				logger.error(sm.getString("spnegoAuthenticator.obtainFailed"), e);
132 
133 				sendInternalServerError(request, response, "spnegoAuthenticator.obtainFailed");
134 				return false;
135 			}
136 
137 			final GSSManager manager = GSSManager.getInstance();
138 			final PrivilegedExceptionAction<GSSCredential> action = () -> manager.createCredential(
139 					null, GSSCredential.INDEFINITE_LIFETIME, SPNEGO_MECHANISM, GSSCredential.ACCEPT_ONLY);
140 
141 			try {
142 				gssContext = manager.createContext(Subject.doAs(lc.getSubject(), action));
143 			} catch (PrivilegedActionException e) {
144 				logger.error(sm.getString("spnegoAuthenticator.obtainFailed"), e.getException());
145 
146 				sendInternalServerError(request, response, "spnegoAuthenticator.obtainFailed");
147 				return false;
148 			} catch (GSSException e) {
149 				logger.error(sm.getString("spnegoAuthenticator.createContextFailed"), e);
150 
151 				sendInternalServerError(request, response, "spnegoAuthenticator.createContextFailed");
152 				return false;
153 			}
154 
155 			try {
156 				outToken = gssContext.acceptSecContext(inToken, 0, inToken.length);
157 			} catch (GSSException e) {
158 				logger.warn(sm.getString("spnegoAuthenticator.invalidToken", authorizationValue), e);
159 
160 				sendUnauthorized(
161 						request, response, SPNEGO_AUTH_SCHEME, "spnegoAuthenticator.invalidToken.responseMessage");
162 				return false;
163 			}
164 
165 			try {
166 				if (gssContext.isEstablished()) {
167 					if (logger.isDebugEnabled())
168 						logger.debug(sm.getString("spnegoAuthenticator.contextSuccessfullyEstablished"));
169 
170 					Realm realm = context.getRealm();
171 					principal = realm.authenticate(gssContext, isStoreDelegatedCredential());
172 
173 					if (principal == null) {
174 						GSSName srcName = gssContext.getSrcName();
175 						sendUnauthorized(
176 								request, response, SPNEGO_AUTH_SCHEME, "gssAuthenticatorBase.userNotFound", srcName);
177 						return false;
178 					}
179 				} else {
180 					logger.error(sm.getString("spnegoAuthenticator.continueContextNotSupported"));
181 
182 					sendInternalServerError(
183 							request, response, "spnegoAuthenticator.continueContextNotSupported.responseMessage");
184 					return false;
185 				}
186 			} catch (GSSException e) {
187 				logger.error(sm.getString("gssAuthenticatorBase.inquireNameFailed"), e);
188 
189 				sendInternalServerError(request, response, "gssAuthenticatorBase.inquireNameFailed");
190 				return false;
191 			}
192 		} finally {
193 			if (gssContext != null) {
194 				try {
195 					gssContext.dispose();
196 				} catch (GSSException e) {
197 					; // Ignore
198 				}
199 			}
200 			if (lc != null) {
201 				try {
202 					lc.logout();
203 				} catch (LoginException e) {
204 					; // Ignore
205 				}
206 			}
207 		}
208 
209 		register(request, response, principal, SPNEGO_METHOD, principal.getName(), null);
210 
211 		if (outToken != null) {
212 			String authenticationValue = Base64.getEncoder().encodeToString(outToken);
213 			if (logger.isDebugEnabled())
214 				logger.debug(sm.getString("spnegoAuthenticator.respondingWithToken", authenticationValue));
215 
216 			response.setHeader(AUTH_HEADER_NAME, SPNEGO_AUTH_SCHEME + " " + authenticationValue);
217 		}
218 
219 		return true;
220 	}
221 
222 	@Override
223 	protected boolean isPreemptiveAuthPossible(Request request) {
224 		String authorization = request.getHeader("Authorization");
225 
226 		return StringUtils.startsWithIgnoreCase(authorization, SPNEGO_AUTH_SCHEME);
227 	}
228 
229 	@Override
230 	protected String getAuthMethod() {
231 		return SPNEGO_METHOD;
232 	}
233 }