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