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