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