Олицетворение Kerberos с использованием SSPI: без ошибок, но не работает
Мне нужно выдать себя за пользователя на сервере приложений Java и выполнить http-запрос к приложению ASP в IIS с разрешениями этого пользователя. Я пытаюсь адаптировать класс WindowsNegotiateScheme Apache HttpClient для этой цели. При этом JNA используется для прямого доступа к функциям аутентификации Windows SSPI, поэтому проблема, вероятно, не относится к Java. Поскольку мне нужен "переход по протоколу", у меня нет пароля пользователя для олицетворения, только имя. Но используя возможности S4U, это должно быть возможно.
Все вызовы функций SSPI возвращают 0 (успех) или 590610 (SEC_I_CONTINUE_NEEDED), никогда не код ошибки, и код выполняется, но запрос http поступает на сторону ASP с пользователем, выполняющим сервер приложений, независимо от того, запускаю ли я его локально на моей рабочей станции под моим идентификатором пользователя или на сервере приложений (который является машиной, на которой также выполняется приложение IIS/ASP), которая работает как локальная система.
Соответствующая часть кода ниже:
public Header authenticate(Credentials credentials, HttpRequest request, HttpContext context) throws AuthenticationException {
final String response;
if (clientCred == null) { // first time
boolean impersonate = ...; // logic not relevant here
try {
final String username = impersonate ? credentials.getUserPrincipal().getName() : CurrentWindowsCredentials.getCurrentUsername();
final Sspi.TimeStamp lifetime = new Sspi.TimeStamp();
this.clientCred = new Sspi.CredHandle();
int credUse = impersonate ? (Sspi.SECPKG_CRED_OUTBOUND | Sspi.SECPKG_CRED_INBOUND) : Sspi.SECPKG_CRED_OUTBOUND;
int rc = Secur32.INSTANCE.AcquireCredentialsHandle(username,
scheme, credUse, null, null, null, null,
clientCred, lifetime);
if (WinError.SEC_E_OK != rc) {
throw new Win32Exception(rc);
}
if(impersonate) {
impersonationContext = getLocalContext(clientCred);
rc = Secur32.INSTANCE.ImpersonateSecurityContext(impersonationContext);
} else {
impersonationContext = null;
}
String targetSpn = getServicePrincipalName(context);
response = getToken(null, null, targetSpn);
} catch (RuntimeException ex) {
...
}
} else {
... // second round
}
... // build header form response and return it
}
метод getLocalContext()
предназначен для получения контекста для самой службы для выдающего себя пользователя. Это выглядит следующим образом:
private static Sspi.CtxtHandle getLocalContext(final Sspi.CredHandle initialCredentials) {
final IntByReference attr = new IntByReference();
String localSpn = "HOST/" + Kernel32Util.getComputerName().toLowerCase();
Sspi.CtxtHandle clientContext = null;
Sspi.CtxtHandle serverContext = null;
Sspi.SecBufferDesc clientToServerToken;
Sspi.SecBufferDesc serverToClientToken = null;
while(true) {
// get clientContext and clientToServerToken:
Sspi.CtxtHandle outClientContext = (clientContext != null) ? clientContext : new Sspi.CtxtHandle();
clientToServerToken = new Sspi.SecBufferDesc(Sspi.SECBUFFER_TOKEN, Sspi.MAX_TOKEN_SIZE);
int rc = Secur32.INSTANCE.InitializeSecurityContext(initialCredentials,
clientContext, localSpn, Sspi.ISC_REQ_DELEGATE | Sspi.ISC_REQ_MUTUAL_AUTH, 0,
Sspi.SECURITY_NATIVE_DREP, serverToClientToken, 0, outClientContext, clientToServerToken,
attr, null);
clientContext = outClientContext; // make them same for next round
switch (rc) {
case WinError.SEC_I_CONTINUE_NEEDED:
break;
case WinError.SEC_E_OK:
return serverContext != null ? serverContext : clientContext;
default:
freeContexts(clientContext, serverContext);
throw new Win32Exception(rc);
}
// get serverContext and serverToClientToken:
Sspi.CtxtHandle outServerContext = (serverContext != null) ? serverContext : new Sspi.CtxtHandle();
serverToClientToken = new Sspi.SecBufferDesc(Sspi.SECBUFFER_TOKEN, Sspi.MAX_TOKEN_SIZE);
rc = Secur32.INSTANCE.AcceptSecurityContext(initialCredentials,
serverContext, clientToServerToken, Sspi.ISC_REQ_DELEGATE | Sspi.ISC_REQ_MUTUAL_AUTH,
Sspi.SECURITY_NATIVE_DREP, outServerContext, serverToClientToken, attr, null);
serverContext = outServerContext; // make them same for next round
switch (rc) {
case WinError.SEC_I_CONTINUE_NEEDED:
break;
case WinError.SEC_E_OK:
return serverContext;
default:
freeContexts(clientContext, serverContext);
throw new Win32Exception(rc);
}
}
}
Я обнаружил, что он проходит два полных раунда (InitializeSecurityContext
, AcceptSecurityContext
, InitializeSecurityContext
, AcceptSecurityContext
) до возвращения.
И для справки, getToken
метод только что скопирован из оригинального WinHttpClient WindowsNegotiateScheme
учебный класс:
String getToken(
final CtxtHandle continueCtx,
final SecBufferDesc continueToken,
final String targetName) {
final IntByReference attr = new IntByReference();
final SecBufferDesc token = new SecBufferDesc(
Sspi.SECBUFFER_TOKEN, Sspi.MAX_TOKEN_SIZE);
sspiContext = new CtxtHandle();
final int rc = Secur32.INSTANCE.InitializeSecurityContext(clientCred,
continueCtx, targetName, Sspi.ISC_REQ_DELEGATE | Sspi.ISC_REQ_MUTUAL_AUTH, 0,
Sspi.SECURITY_NATIVE_DREP, continueToken, 0, sspiContext, token,
attr, null);
switch (rc) {
case WinError.SEC_I_CONTINUE_NEEDED:
continueNeeded = true;
break;
case WinError.SEC_E_OK:
dispose(); // Don't keep the context
continueNeeded = false;
break;
default:
dispose();
throw new Win32Exception(rc);
}
return Base64.encodeBase64String(token.getBytes());
}
Я проверил в отладчике, что имя пользователя, используемое для олицетворения, на самом деле что-то вроде DOMAINNAME\johndoe
и я понял, что это даже не имеет значения, если я использую неискусного пользователя, поведение точно такое же: AcquireCredentialsHandle
возвращает 0 (= SEC_E_OK). Это говорит мне о том, что либо этот метод не проверяет имя пользователя по контроллеру домена, но только синтаксис его аргументов, но тогда почему первоначальный вызов InitializeSecurityContext
не жалуетесь, если это первый аргумент учетных данных для несуществующего пользователя? Или метод как-то вообще игнорирует свой первый аргумент, но почему?
Предполагая, что у меня есть контекст, чтобы пользователь выдал себя за AcquireCredentialsHandle
позвонить и пропустить вызов getLocalContext
а также ImpersonateSecurityContext
тоже не сработало.
Я обнаружил, что не так много документации по использованию чистого SSPI для олицетворения. Большинство образцов, которые я обнаружил (как это), основаны на классе.net WindowsIdentity
, который не доступен в чистом интерфейсе SSPI. И исходный код этого класса не выглядит так, как будто он состоит только из нескольких вызовов SSPI, но на него ссылается множество других инфраструктур.net.
Я думал, что, возможно, олицетворение ОС Windows не работает должным образом с Java, но согласно этому вопросу олицетворение Windows относится к потокам, и согласно этому вопросу Java использует потоки ОС.
Как я могу заставить олицетворение работать, или у вас есть хотя бы какие-то подсказки, что попробовать?
1 ответ
Ваш код никогда не будет делать то, что вы описали. Ваши фрагменты выполняют делегирование полномочий (неограниченное делегирование) только в том случае, если перенаправленный TGT встроен в контекст безопасности. Поскольку вы хотите переход по протоколу (S4U2Self), вам необходимо позвонить LsaLogonUser
с KERB_S4U_LOGON
,
Прочитайте этот пост в блоге. Это будет очень тяжело в Си. Никогда не понимал, почему это так легко с GSS-API, но такая боль в Windows.
Удачи.