Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ public class KerberosConstants {


/**
* Internal attribute used in "state" map . Contains token to be passed in HTTP Response back to browser to continue handshake
* Internal attribute used in "state" map. Contains a token to be passed in the HTTP response to continue or complete the handshake.
*/
public static final String RESPONSE_TOKEN = "SpnegoResponseToken";

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,11 @@ public CredentialValidationOutput authenticate(RealmModel realm, CredentialInput

return CredentialValidationOutput.fallback();
} else {
String responseToken = spnegoAuthenticator.getResponseToken();
if (responseToken != null) {
state.put(KerberosConstants.RESPONSE_TOKEN, responseToken);
}

String delegationCredential = spnegoAuthenticator.getSerializedDelegationCredential();
if (delegationCredential != null) {
state.put(KerberosConstants.GSS_DELEGATION_CREDENTIAL, delegationCredential);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,10 @@ protected GSSContext establishContext() throws GSSException {

byte[] inputToken = Base64.getMimeDecoder().decode(spnegoToken);
byte[] respToken = gssContext.acceptSecContext(inputToken, 0, inputToken.length);
responseToken = Base64.getEncoder().encodeToString(respToken);
if (respToken != null && respToken.length > 0) {
// MIME Base64 can insert CRLF and must not be used for a value sent in an HTTP header.
responseToken = Base64.getEncoder().encodeToString(respToken);
}

return gssContext;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -981,6 +981,11 @@ public CredentialValidationOutput authenticate(RealmModel realm, CredentialInput
credential.setNote(KerberosConstants.AUTHENTICATED_SPNEGO_CONTEXT, spnegoAuthenticator);
return CredentialValidationOutput.fallback();
} else {
String responseToken = spnegoAuthenticator.getResponseToken();
if (responseToken != null) {
state.put(KerberosConstants.RESPONSE_TOKEN, responseToken);
}

String delegationCredential = spnegoAuthenticator.getSerializedDelegationCredential();
if (delegationCredential != null) {
state.put(KerberosConstants.GSS_DELEGATION_CREDENTIAL, delegationCredential);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
package org.keycloak.authentication.authenticators.browser;

import java.net.URI;
import java.util.HashMap;
import java.util.Map;

import jakarta.ws.rs.core.HttpHeaders;
Expand Down Expand Up @@ -103,9 +104,14 @@ public void authenticate(AuthenticationFlowContext context) {
if (output.getAuthStatus() == CredentialValidationOutput.Status.AUTHENTICATED) {
context.setUser(output.getAuthenticatedUser());
if (output.getState() != null && !output.getState().isEmpty()) {
for (Map.Entry<String, String> entry : output.getState().entrySet()) {
context.getAuthenticationSession().setUserSessionNote(entry.getKey(), entry.getValue());
Map<String, String> state = new HashMap<>(output.getState());
String spnegoResponseToken = state.remove(KerberosConstants.RESPONSE_TOKEN);
if (spnegoResponseToken != null && !spnegoResponseToken.isEmpty()) {
String negotiateHeader = KerberosConstants.NEGOTIATE + " " + spnegoResponseToken;
// The authenticator does not own the final flow response, so attach the final GSS token before the flow continues.
context.getSession().getContext().getHttpResponse().setHeader(HttpHeaders.WWW_AUTHENTICATE, negotiateHeader);
}
state.forEach(context.getAuthenticationSession()::setUserSessionNote);
}
context.success(UserCredentialModel.KERBEROS);
} else if (output.getAuthStatus() == CredentialValidationOutput.Status.CONTINUE) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@

package org.keycloak.testsuite.federation.kerberos;

import java.util.Base64;
import java.util.List;
import java.util.concurrent.atomic.AtomicReference;

Expand All @@ -42,6 +43,7 @@
import org.keycloak.testsuite.util.TestAppHelper;
import org.keycloak.testsuite.util.oauth.AccessTokenResponse;

import org.ietf.jgss.GSSContext;
import org.ietf.jgss.GSSCredential;
import org.junit.Assume;
import org.junit.Ignore;
Expand Down Expand Up @@ -118,6 +120,51 @@ public void spnegoLoginWithRequiredKerberosAuthExecutionTest() {
Assertions.assertEquals(302, response.getStatus());
}

@Test
public void spnegoMutualAuthenticationTest() throws Exception {
ProtocolMapperModel protocolMapper = UserSessionNoteMapper.createClaimMapper("SPNEGO response token",
KerberosConstants.RESPONSE_TOKEN, KerberosConstants.RESPONSE_TOKEN, "String",
true, false, true, true);
Comment on lines +125 to +127

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably the mapper to test the responseMapper is not added into the session is not needed. But OK, once is done, it's better.

ProtocolMapperRepresentation protocolMapperRep = ModelToRepresentation.toRepresentation(protocolMapper);
ClientResource clientResource = findClientByClientId(testRealmResource(), "kerberos-app");
Response mapperResponse = clientResource.getProtocolMappers().createMapper(protocolMapperRep);
String protocolMapperId = ApiUtil.getCreatedId(mapperResponse);
mapperResponse.close();

try {
Response response = spnegoLoginWithoutRedirect("hnelson", "secret");
String codeUrl;
try {
Assertions.assertEquals(302, response.getStatus());
codeUrl = response.getLocation().toString();

String negotiatePrefix = KerberosConstants.NEGOTIATE + " ";
String negotiateHeader = response.getHeaderString(HttpHeaders.WWW_AUTHENTICATE);
Assertions.assertNotNull(negotiateHeader);
Assertions.assertTrue(negotiateHeader.startsWith(negotiatePrefix));

byte[] responseToken = Base64.getDecoder().decode(negotiateHeader.substring(negotiatePrefix.length()));
GSSContext gssContext = spnegoSchemeFactory.getGssContext();
Assertions.assertNotNull(gssContext);
try {
gssContext.initSecContext(responseToken, 0, responseToken.length);
Assertions.assertTrue(gssContext.isEstablished());
Assertions.assertTrue(gssContext.getMutualAuthState());
} finally {
gssContext.dispose();
}
} finally {
response.close();
}

AccessTokenResponse tokenResponse = assertAuthenticationSuccess(codeUrl);
AccessToken token = oauth.verifyToken(tokenResponse.getAccessToken());
Assertions.assertFalse(token.getOtherClaims().containsKey(KerberosConstants.RESPONSE_TOKEN));
} finally {
clientResource.getProtocolMappers().delete(protocolMapperId);
}
}


// KEYCLOAK-2102
@Test
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -243,11 +243,7 @@ protected String invokeLdap(GSSCredential gssCredential, String username) throws


protected Response spnegoLogin(String username, String password) {
String kcLoginPageLocation = oauth.loginForm().state("spnegoLogin").build();

// Request for SPNEGO login sent with Resteasy client
spnegoSchemeFactory.setCredentials(username, password);
Response response = client.target(kcLoginPageLocation).request().get();
Response response = spnegoLoginWithoutRedirect(username, password);
if (response.getStatus() == 302) {
if (response.getLocation() == null)
return response;
Expand All @@ -260,6 +256,15 @@ protected Response spnegoLogin(String username, String password) {

}

protected Response spnegoLoginWithoutRedirect(String username, String password) {
String kcLoginPageLocation = oauth.loginForm().state("spnegoLogin").build();

// Request for SPNEGO login sent with Resteasy client
spnegoSchemeFactory.setCredentials(username, password);
return client.target(kcLoginPageLocation).request().get();

}


protected void initHttpClient(boolean useSpnego) {
if (client != null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ public class KeycloakSPNegoSchemeFactory extends SPNegoSchemeFactory {

private String username;
private String password;
private GSSContext gssContext;


public KeycloakSPNegoSchemeFactory(CommonKerberosConfig kerberosConfig, boolean credDelegEnabled) {
Expand All @@ -61,6 +62,10 @@ public void setCredentials(String username, String password) {
this.password = password;
}

public GSSContext getGssContext() {
return gssContext;
}


@Override
@SuppressWarnings("deprecation")
Expand Down Expand Up @@ -120,7 +125,7 @@ public ByteArrayHolder run() throws Exception {
GSSManager manager = getManager();
String httPrincipal = kerberosConfig.getServerPrincipal().replaceFirst("/.*@", "/" + authServer + "@");
GSSName serverName = manager.createName(httPrincipal, null);
GSSContext gssContext = manager.createContext(
gssContext = manager.createContext(
serverName.canonicalize(oid), oid, null, GSSContext.DEFAULT_LIFETIME);
gssContext.requestMutualAuth(true);
gssContext.requestCredDeleg(credDelegEnabled);
Expand Down
Loading