1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27 package org.apache.hc.client5.http.impl.win;
28
29 import java.security.Principal;
30
31 import org.apache.hc.client5.http.utils.Base64;
32 import org.apache.hc.client5.http.RouteInfo;
33 import org.apache.hc.client5.http.auth.AuthChallenge;
34 import org.apache.hc.client5.http.auth.AuthScheme;
35 import org.apache.hc.client5.http.auth.AuthenticationException;
36 import org.apache.hc.client5.http.auth.BasicUserPrincipal;
37 import org.apache.hc.client5.http.auth.ChallengeType;
38 import org.apache.hc.client5.http.auth.CredentialsProvider;
39 import org.apache.hc.client5.http.auth.MalformedChallengeException;
40 import org.apache.hc.client5.http.auth.StandardAuthScheme;
41 import org.apache.hc.client5.http.protocol.HttpClientContext;
42 import org.apache.hc.core5.annotation.Experimental;
43 import org.apache.hc.core5.http.HttpHost;
44 import org.apache.hc.core5.http.HttpRequest;
45 import org.apache.hc.core5.http.protocol.HttpContext;
46 import org.apache.hc.core5.net.URIAuthority;
47 import org.apache.hc.core5.util.Args;
48 import org.apache.hc.core5.util.TextUtils;
49 import org.slf4j.Logger;
50 import org.slf4j.LoggerFactory;
51
52 import com.sun.jna.platform.win32.Secur32;
53 import com.sun.jna.platform.win32.Secur32Util;
54 import com.sun.jna.platform.win32.Sspi;
55 import com.sun.jna.platform.win32.Sspi.CredHandle;
56 import com.sun.jna.platform.win32.Sspi.CtxtHandle;
57 import com.sun.jna.platform.win32.Sspi.SecBufferDesc;
58 import com.sun.jna.platform.win32.Sspi.TimeStamp;
59 import com.sun.jna.platform.win32.SspiUtil.ManagedSecBufferDesc;
60 import com.sun.jna.platform.win32.Win32Exception;
61 import com.sun.jna.platform.win32.WinError;
62 import com.sun.jna.ptr.IntByReference;
63
64
65
66
67
68
69
70
71
72 @Experimental
73 public class WindowsNegotiateScheme implements AuthScheme {
74
75 private static final Logger LOG = LoggerFactory.getLogger(WindowsNegotiateScheme.class);
76
77
78 private final String schemeName;
79 private final String servicePrincipalName;
80
81 private ChallengeType challengeType;
82 private String challenge;
83 private CredHandle clientCred;
84 private CtxtHandle sspiContext;
85 private boolean continueNeeded;
86
87 WindowsNegotiateScheme(final String schemeName, final String servicePrincipalName) {
88 super();
89
90 this.schemeName = (schemeName == null) ? StandardAuthScheme.SPNEGO : schemeName;
91 this.continueNeeded = true;
92 this.servicePrincipalName = servicePrincipalName;
93
94 if (LOG.isDebugEnabled()) {
95 LOG.debug("Created WindowsNegotiateScheme using {}", this.schemeName);
96 }
97 }
98
99 public void dispose() {
100 if (clientCred != null && !clientCred.isNull()) {
101 final int rc = Secur32.INSTANCE.FreeCredentialsHandle(clientCred);
102 if (WinError.SEC_E_OK != rc) {
103 throw new Win32Exception(rc);
104 }
105 }
106 if (sspiContext != null && !sspiContext.isNull()) {
107 final int rc = Secur32.INSTANCE.DeleteSecurityContext(sspiContext);
108 if (WinError.SEC_E_OK != rc) {
109 throw new Win32Exception(rc);
110 }
111 }
112 continueNeeded = true;
113 clientCred = null;
114 sspiContext = null;
115 }
116
117 @Override
118 public String getName() {
119 return schemeName;
120 }
121
122 @Override
123 public boolean isConnectionBased() {
124 return true;
125 }
126
127 @Override
128 public String getRealm() {
129 return null;
130 }
131
132 @Override
133 public void processChallenge(
134 final AuthChallenge authChallenge,
135 final HttpContext context) throws MalformedChallengeException {
136 Args.notNull(authChallenge, "AuthChallenge");
137 challengeType = authChallenge.getChallengeType();
138 challenge = authChallenge.getValue();
139 if (TextUtils.isBlank(challenge)) {
140 if (clientCred != null) {
141 dispose();
142 if (continueNeeded) {
143 throw new IllegalStateException("Unexpected token");
144 }
145 }
146 }
147 }
148
149 @Override
150 public boolean isResponseReady(
151 final HttpHost host,
152 final CredentialsProvider credentialsProvider,
153 final HttpContext context) throws AuthenticationException {
154 return true;
155 }
156
157
158
159
160
161
162 public static String getCurrentUsername() {
163 return Secur32Util.getUserNameEx(Secur32.EXTENDED_NAME_FORMAT.NameSamCompatible);
164 }
165
166 @Override
167 public Principal getPrincipal() {
168 return new BasicUserPrincipal(getCurrentUsername());
169 }
170
171 @Override
172 public String generateAuthResponse(
173 final HttpHost host,
174 final HttpRequest request,
175 final HttpContext context) throws AuthenticationException {
176
177 final HttpClientContext clientContext = HttpClientContext.adapt(context);
178 final String response;
179 if (clientCred == null) {
180
181 try {
182 final String username = getCurrentUsername();
183 final TimeStamp lifetime = new TimeStamp();
184
185 clientCred = new CredHandle();
186 final int rc = Secur32.INSTANCE.AcquireCredentialsHandle(username,
187 schemeName, Sspi.SECPKG_CRED_OUTBOUND, null, null, null, null,
188 clientCred, lifetime);
189
190 if (WinError.SEC_E_OK != rc) {
191 throw new Win32Exception(rc);
192 }
193
194 final String targetName = getServicePrincipalName(request, clientContext);
195 response = getToken(null, null, targetName);
196 } catch (final RuntimeException ex) {
197 failAuthCleanup();
198 if (ex instanceof Win32Exception) {
199 throw new AuthenticationException("Authentication Failed", ex);
200 }
201 throw ex;
202 }
203 } else if (challenge == null || challenge.isEmpty()) {
204 failAuthCleanup();
205 throw new AuthenticationException("Authentication Failed");
206 } else {
207 try {
208 final byte[] continueTokenBytes = Base64.decodeBase64(challenge);
209 final SecBufferDesc continueTokenBuffer = new ManagedSecBufferDesc(
210 Sspi.SECBUFFER_TOKEN, continueTokenBytes);
211 final String targetName = getServicePrincipalName(request, clientContext);
212 response = getToken(this.sspiContext, continueTokenBuffer, targetName);
213 } catch (final RuntimeException ex) {
214 failAuthCleanup();
215 if (ex instanceof Win32Exception) {
216 throw new AuthenticationException("Authentication Failed", ex);
217 }
218 throw ex;
219 }
220 }
221 return schemeName + " " + response;
222 }
223
224 private void failAuthCleanup() {
225 dispose();
226 this.continueNeeded = false;
227 }
228
229
230
231
232
233
234 private String getServicePrincipalName(final HttpRequest request, final HttpClientContext clientContext) {
235 String spn = null;
236 if (this.servicePrincipalName != null) {
237 spn = this.servicePrincipalName;
238 } else if (challengeType == ChallengeType.PROXY) {
239 final RouteInfo route = clientContext.getHttpRoute();
240 if (route != null) {
241 spn = "HTTP/" + route.getProxyHost().getHostName();
242 }
243 } else {
244 final URIAuthority authority = request.getAuthority();
245 if (authority != null) {
246 spn = "HTTP/" + authority.getHostName();
247 } else {
248 final RouteInfo route = clientContext.getHttpRoute();
249 if (route != null) {
250 spn = "HTTP/" + route.getTargetHost().getHostName();
251 }
252 }
253 }
254 if (LOG.isDebugEnabled()) {
255 LOG.debug("Using SPN: {}", spn);
256 }
257 return spn;
258 }
259
260
261 String getToken(
262 final CtxtHandle continueCtx,
263 final SecBufferDesc continueToken,
264 final String targetName) {
265 final IntByReference attr = new IntByReference();
266 final ManagedSecBufferDesc token = new ManagedSecBufferDesc(
267 Sspi.SECBUFFER_TOKEN, Sspi.MAX_TOKEN_SIZE);
268
269 sspiContext = new CtxtHandle();
270 final int rc = Secur32.INSTANCE.InitializeSecurityContext(clientCred,
271 continueCtx, targetName, Sspi.ISC_REQ_DELEGATE | Sspi.ISC_REQ_MUTUAL_AUTH, 0,
272 Sspi.SECURITY_NATIVE_DREP, continueToken, 0, sspiContext, token,
273 attr, null);
274 switch (rc) {
275 case WinError.SEC_I_CONTINUE_NEEDED:
276 continueNeeded = true;
277 break;
278 case WinError.SEC_E_OK:
279 dispose();
280 continueNeeded = false;
281 break;
282 default:
283 dispose();
284 throw new Win32Exception(rc);
285 }
286 return Base64.encodeBase64String(token.getBuffer(0).getBytes());
287 }
288
289 @Override
290 public boolean isChallengeComplete() {
291 return !continueNeeded;
292 }
293
294 }