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