Merge pull request #966 from mposolda/master

Kerberos broker fixes
This commit is contained in:
Marek Posolda 2015-02-11 13:13:42 +01:00
commit 37a8e295bd
17 changed files with 116 additions and 83 deletions

View file

@ -2,10 +2,10 @@
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<parent>
<artifactId>keycloak-broker-parent</artifactId>
<artifactId>keycloak-parent</artifactId>
<groupId>org.keycloak</groupId>
<version>1.2.0.Beta1-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
<relativePath>../../pom.xml</relativePath>
</parent>
<modelVersion>4.0.0</modelVersion>

View file

@ -19,6 +19,7 @@ package org.keycloak.broker.provider;
import org.jboss.resteasy.spi.HttpRequest;
import org.keycloak.models.ClientSessionModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import javax.ws.rs.core.UriInfo;
@ -28,6 +29,7 @@ import javax.ws.rs.core.UriInfo;
*/
public class AuthenticationRequest {
private final KeycloakSession session;
private final UriInfo uriInfo;
private final String state;
private final HttpRequest httpRequest;
@ -35,7 +37,8 @@ public class AuthenticationRequest {
private final String redirectUri;
private final ClientSessionModel clientSession;
public AuthenticationRequest(RealmModel realm, ClientSessionModel clientSession, HttpRequest httpRequest, UriInfo uriInfo, String state, String redirectUri) {
public AuthenticationRequest(KeycloakSession session, RealmModel realm, ClientSessionModel clientSession, HttpRequest httpRequest, UriInfo uriInfo, String state, String redirectUri) {
this.session = session;
this.realm = realm;
this.httpRequest = httpRequest;
this.uriInfo = uriInfo;
@ -44,6 +47,10 @@ public class AuthenticationRequest {
this.clientSession = clientSession;
}
public KeycloakSession getSession() {
return session;
}
public UriInfo getUriInfo() {
return this.uriInfo;
}

View file

@ -2,10 +2,10 @@
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<parent>
<artifactId>keycloak-broker-parent</artifactId>
<artifactId>keycloak-parent</artifactId>
<groupId>org.keycloak</groupId>
<version>1.2.0.Beta1-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
<relativePath>../../pom.xml</relativePath>
</parent>
<modelVersion>4.0.0</modelVersion>
@ -20,6 +20,11 @@
<artifactId>keycloak-broker-core</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-login-api</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.jboss.logging</groupId>
<artifactId>jboss-logging</artifactId>

View file

@ -22,4 +22,9 @@ public class KerberosConstants {
*/
public static final String SPNEGO_OID = "1.3.6.1.5.5.2";
/**
* OID of Kerberos v5 mechanism. See http://www.oid-info.com/get/1.2.840.113554.1.2.2
*/
public static final String KRB5_OID = "1.2.840.113554.1.2.2";
}

View file

@ -3,6 +3,7 @@ package org.keycloak.broker.kerberos;
import java.net.URI;
import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.UriBuilder;
import javax.ws.rs.core.UriInfo;
@ -14,6 +15,7 @@ import org.keycloak.broker.provider.AbstractIdentityProvider;
import org.keycloak.broker.provider.AuthenticationRequest;
import org.keycloak.broker.provider.AuthenticationResponse;
import org.keycloak.broker.provider.FederatedIdentity;
import org.keycloak.login.LoginFormsProvider;
import org.keycloak.models.FederatedIdentityModel;
/**
@ -30,8 +32,6 @@ public class KerberosIdentityProvider extends AbstractIdentityProvider<KerberosI
@Override
public AuthenticationResponse handleRequest(AuthenticationRequest request) {
// TODO: trace
logger.info("handleRequest");
// Just redirect to handleResponse for now
URI redirectUri = UriBuilder.fromUri(request.getRedirectUri()).queryParam(KerberosConstants.RELAY_STATE_PARAM, request.getState()).build();
@ -56,16 +56,16 @@ public class KerberosIdentityProvider extends AbstractIdentityProvider<KerberosI
// Case when we don't yet have any Negotiate header
if (authHeader == null) {
return sendNegotiateResponse(null);
return sendNegotiateResponse(request, null);
}
String[] tokens = authHeader.split(" ");
if (tokens.length != 2) {
logger.warn("Invalid length of tokens: " + tokens.length);
return sendNegotiateResponse(null);
return sendNegotiateResponse(request, null);
} else if (!KerberosConstants.NEGOTIATE.equalsIgnoreCase(tokens[0])) {
logger.warn("Unknown scheme " + tokens[0]);
return sendNegotiateResponse(null);
return sendNegotiateResponse(request, null);
} else {
String spnegoToken = tokens[1];
SPNEGOAuthenticator spnegoAuthenticator = createSPNEGOAuthenticator(spnegoToken);
@ -75,7 +75,7 @@ public class KerberosIdentityProvider extends AbstractIdentityProvider<KerberosI
FederatedIdentity federatedIdentity = getFederatedIdentity(spnegoAuthenticator);
return AuthenticationResponse.end(federatedIdentity);
} else {
return sendNegotiateResponse(spnegoAuthenticator.getResponseToken());
return sendNegotiateResponse(request, spnegoAuthenticator.getResponseToken());
}
}
}
@ -96,12 +96,22 @@ public class KerberosIdentityProvider extends AbstractIdentityProvider<KerberosI
* @param negotiateToken token to be send back in response or null if just "WWW-Authenticate: Negotiate" should be sent
* @return AuthenticationResponse
*/
protected AuthenticationResponse sendNegotiateResponse(String negotiateToken) {
protected AuthenticationResponse sendNegotiateResponse(AuthenticationRequest request, String negotiateToken) {
String negotiateHeader = negotiateToken == null ? KerberosConstants.NEGOTIATE : KerberosConstants.NEGOTIATE + " " + negotiateToken;
Response response = Response.status(Response.Status.UNAUTHORIZED)
.header(HttpHeaders.WWW_AUTHENTICATE, negotiateHeader)
.build();
if (logger.isTraceEnabled()) {
logger.trace("Sending back " + HttpHeaders.WWW_AUTHENTICATE + ": " + negotiateHeader);
}
// Error page is rendered just if browser is unable to send Authorization header with SPNEGO token
Response response = request.getSession().getProvider(LoginFormsProvider.class)
.setRealm(request.getRealm())
.setUriInfo(request.getUriInfo())
.setError("errorKerberosLogin")
.setStatus(Response.Status.UNAUTHORIZED)
.createErrorPage();
response.getMetadata().putSingle(HttpHeaders.WWW_AUTHENTICATE, negotiateHeader);
return AuthenticationResponse.fromResponse(response);
}
@ -111,7 +121,7 @@ public class KerberosIdentityProvider extends AbstractIdentityProvider<KerberosI
FederatedIdentity user = new FederatedIdentity(kerberosUsername);
user.setUsername(kerberosUsername);
// Just guessing email, but likely can't do anything better...
// Just guessing email
String[] tokens = kerberosUsername.split("@");
String email = tokens[0] + "@" + tokens[1].toLowerCase();
user.setEmail(email);

View file

@ -1,6 +1,5 @@
package org.keycloak.broker.kerberos;
import org.keycloak.broker.kerberos.KerberosIdentityProvider;
import org.keycloak.broker.provider.AbstractIdentityProviderFactory;
import org.keycloak.models.IdentityProviderModel;

View file

@ -1,6 +1,7 @@
package org.keycloak.broker.kerberos.impl;
import java.io.IOException;
import java.security.PrivilegedActionException;
import java.security.PrivilegedExceptionAction;
import javax.security.auth.Subject;
@ -10,16 +11,14 @@ import org.ietf.jgss.GSSContext;
import org.ietf.jgss.GSSCredential;
import org.ietf.jgss.GSSException;
import org.ietf.jgss.GSSManager;
import org.ietf.jgss.Oid;
import org.jboss.logging.Logger;
import org.keycloak.broker.kerberos.KerberosConstants;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class SPNEGOAuthenticator {
private static final Logger logger = Logger.getLogger(SPNEGOAuthenticator.class);
private static final Logger log = Logger.getLogger(SPNEGOAuthenticator.class);
private static final GSSManager GSS_MANAGER = GSSManager.getInstance();
@ -37,18 +36,21 @@ public class SPNEGOAuthenticator {
}
public void authenticate() {
// TODO: debug
logger.info("SPNEGO Login with token: " + spnegoToken);
if (log.isTraceEnabled()) {
log.trace("SPNEGO Login with token: " + spnegoToken);
}
try {
Subject serverSubject = kerberosSubjectAuthenticator.authenticateServerSubject();
authenticated = Subject.doAs(serverSubject, new AcceptSecContext());
} catch (Exception e) {
logger.warn("SPNEGO login failed: " + e.getMessage());
// TODO: debug and check if it is shown in the log
if (logger.isInfoEnabled()) {
logger.info("SPNEGO login failed: " + e.getMessage(), e);
String message = e.getMessage();
if (e instanceof PrivilegedActionException && e.getCause() != null) {
message = e.getCause().getMessage();
}
log.warn("SPNEGO login failed: " + message);
if (log.isDebugEnabled()) {
log.debug("SPNEGO login failed: " + message, e);
}
} finally {
kerberosSubjectAuthenticator.logoutServerSubject();
@ -77,18 +79,21 @@ public class SPNEGOAuthenticator {
public Boolean run() throws Exception {
GSSContext gssContext = null;
try {
// TODO: debug
logger.info("Going to establish security context");
if (log.isTraceEnabled()) {
log.trace("Going to establish security context");
}
gssContext = establishContext();
logAuthDetails(gssContext);
// What should be done with delegation credential? Figure out if there are use-cases for storing it as claims in FederatedIdentity
if (gssContext.getCredDelegState()) {
delegationCredential = gssContext.getDelegCred();
}
if (gssContext.isEstablished()) {
principal = gssContext.getSrcName().toString();
// What should be done with delegation credential? Figure out if there are use-cases for storing it as claims in FederatedIdentity
if (gssContext.getCredDelegState()) {
delegationCredential = gssContext.getDelegCred();
}
return true;
} else {
return false;
@ -103,12 +108,7 @@ public class SPNEGOAuthenticator {
}
protected GSSContext establishContext() throws GSSException, IOException {
Oid spnegoOid = new Oid(KerberosConstants.SPNEGO_OID);
GSSCredential credential = GSS_MANAGER.createCredential(null,
GSSCredential.DEFAULT_LIFETIME,
spnegoOid,
GSSCredential.ACCEPT_ONLY);
GSSContext gssContext = GSS_MANAGER.createContext(credential);
GSSContext gssContext = GSS_MANAGER.createContext((GSSCredential) null);
byte[] inputToken = Base64.decode(spnegoToken);
byte[] respToken = gssContext.acceptSecContext(inputToken, 0, inputToken.length);
@ -118,20 +118,18 @@ public class SPNEGOAuthenticator {
}
protected void logAuthDetails(GSSContext gssContext) throws GSSException {
// TODO: debug
if (logger.isInfoEnabled()) {
if (log.isDebugEnabled()) {
String message = new StringBuilder("SPNEGO Security context accepted with token: " + responseToken)
.append(", established: " + gssContext.isEstablished())
.append(", credDelegState: " + gssContext.getCredDelegState())
.append(", mutualAuthState: " + gssContext.getMutualAuthState())
.append(", lifetime: " + gssContext.getLifetime())
.append(", confState: " + gssContext.getConfState())
.append(", integState: " + gssContext.getIntegState())
.append(", srcName: " + gssContext.getSrcName())
.append(", targName: " + gssContext.getTargName())
.append(", established: ").append(gssContext.isEstablished())
.append(", credDelegState: ").append(gssContext.getCredDelegState())
.append(", mutualAuthState: ").append(gssContext.getMutualAuthState())
.append(", lifetime: ").append(gssContext.getLifetime())
.append(", confState: ").append(gssContext.getConfState())
.append(", integState: ").append(gssContext.getIntegState())
.append(", srcName: ").append(gssContext.getSrcName())
.append(", targName: ").append(gssContext.getTargName())
.toString();
logger.info(message);
log.debug(message);
}
}

View file

@ -2,10 +2,10 @@
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<parent>
<artifactId>keycloak-broker-parent</artifactId>
<artifactId>keycloak-parent</artifactId>
<groupId>org.keycloak</groupId>
<version>1.2.0.Beta1-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
<relativePath>../../pom.xml</relativePath>
</parent>
<modelVersion>4.0.0</modelVersion>

View file

@ -2,10 +2,10 @@
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<parent>
<artifactId>keycloak-broker-parent</artifactId>
<artifactId>keycloak-parent</artifactId>
<groupId>org.keycloak</groupId>
<version>1.2.0.Beta1-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
<relativePath>../../pom.xml</relativePath>
</parent>
<modelVersion>4.0.0</modelVersion>

View file

@ -133,7 +133,6 @@ module.controller('UserFederatedIdentityCtrl', function($scope, realm, user, fed
$scope.realm = realm;
$scope.user = user;
$scope.federatedIdentities = federatedIdentities;
console.log('showing federated identities of user');
});

View file

@ -97,6 +97,8 @@ actionPasswordWarning=You need to change your password to activate your account.
actionEmailWarning=You need to verify your email address to activate your account.
actionFollow=Please fill in the fields below.
errorKerberosLogin=Unable to login with Kerberos
successHeader=Success!
errorHeader=Error!

View file

@ -53,7 +53,7 @@ public class FreeMarkerLoginFormsProvider implements LoginFormsProvider {
private String message;
private String accessCode;
private Response.Status status = Response.Status.OK;
private Response.Status status;
private List<RoleModel> realmRolesRequested;
private MultivaluedMap<String, RoleModel> resourceRolesRequested;
private MultivaluedMap<String, String> queryParams;
@ -218,6 +218,10 @@ public class FreeMarkerLoginFormsProvider implements LoginFormsProvider {
break;
}
if (status == null) {
status = Response.Status.OK;
}
try {
String result = freeMarker.processTemplate(attributes, Templates.getTemplate(page), theme);
Response.ResponseBuilder builder = Response.status(status).type(MediaType.TEXT_HTML).entity(result);
@ -246,7 +250,9 @@ public class FreeMarkerLoginFormsProvider implements LoginFormsProvider {
}
public Response createErrorPage() {
setStatus(Response.Status.INTERNAL_SERVER_ERROR);
if (status == null) {
status = Response.Status.INTERNAL_SERVER_ERROR;
}
return createResponse(LoginFormsPages.ERROR);
}

View file

@ -54,16 +54,16 @@ public interface UserResource {
public List<UserSessionRepresentation> getUserSessions();
@GET
@Path("social-links")
public List<FederatedIdentityRepresentation> getSocialLinks();
@Path("federated-identity")
public List<FederatedIdentityRepresentation> getFederatedIdentity();
@POST
@Path("social-links/{provider}")
public Response addSocialLink(@PathParam("provider") String provider, FederatedIdentityRepresentation rep);
@Path("federated-identity/{provider}")
public Response addFederatedIdentity(@PathParam("provider") String provider, FederatedIdentityRepresentation rep);
@Path("social-links/{provider}")
@Path("federated-identity/{provider}")
@DELETE
public void removeSocialLink(final @PathParam("provider") String provider);
public void removeFederatedIdentity(final @PathParam("provider") String provider);
@Path("role-mappings")
public RoleMappingResource roles();

View file

@ -422,7 +422,7 @@ public class AuthenticationBrokerResource {
}
private AuthenticationRequest createAuthenticationRequest(String providerId, String code, RealmModel realm, ClientSessionModel clientSession) {
return new AuthenticationRequest(realm, clientSession, this.request, this.uriInfo, code, getRedirectUri(providerId, realm));
return new AuthenticationRequest(this.session, realm, clientSession, this.request, this.uriInfo, code, getRedirectUri(providerId, realm));
}
private String getRedirectUri(String providerId, RealmModel realm) {

View file

@ -264,7 +264,7 @@ public class UsersResource {
for (FederatedIdentityModel identity : identities) {
for (IdentityProviderModel identityProviderModel : realm.getIdentityProviders()) {
if (identityProviderModel.getProviderId().equals(identity.getIdentityProvider())) {
if (identityProviderModel.getId().equals(identity.getIdentityProvider())) {
FederatedIdentityRepresentation rep = ModelToRepresentation.toRepresentation(identity);
rep.setIdentityProvider(identityProviderModel.getName());
@ -276,10 +276,10 @@ public class UsersResource {
return result;
}
@Path("{username}/social-links/{provider}")
@Path("{username}/federated-identity/{provider}")
@POST
@NoCache
public Response addSocialLink(final @PathParam("username") String username, final @PathParam("provider") String provider, FederatedIdentityRepresentation rep) {
public Response addFederatedIdentity(final @PathParam("username") String username, final @PathParam("provider") String provider, FederatedIdentityRepresentation rep) {
auth.requireManage();
UserModel user = session.users().getUserByUsername(username, realm);
if (user == null) {
@ -295,10 +295,10 @@ public class UsersResource {
return Response.noContent().build();
}
@Path("{username}/social-links/{provider}")
@Path("{username}/federated-identity/{provider}")
@DELETE
@NoCache
public void removeSocialLink(final @PathParam("username") String username, final @PathParam("provider") String provider) {
public void removeFederatedIdentity(final @PathParam("username") String username, final @PathParam("provider") String provider) {
auth.requireManage();
UserModel user = session.users().getUserByUsername(username, realm);
if (user == null) {

View file

@ -14,9 +14,11 @@ log4j.logger.org.keycloak=info
# Enable to view database updates
# log4j.logger.org.keycloak.connections.jpa.updater.liquibase.LiquibaseJpaUpdaterProvider=debug
# log4j.logger.org.keycloak.connections.mongo.updater.DefaultMongoUpdaterProvider=debug
# log4j.logger.org.keycloak.connections.jpa.DefaultJpaConnectionProviderFactory=debug
# Enable to view kerberos/spnego logging
# log4j.logger.org.keycloak.broker.kerberos=trace
log4j.logger.org.xnio=off
log4j.logger.org.hibernate=off
log4j.logger.org.jboss.resteasy=warn

View file

@ -114,7 +114,7 @@ public class UserTest extends AbstractClientTest {
}
@Test
public void addSocialLink() {
public void addFederatedIdentity() {
createUser();
UserResource user = realm.users().get("user1");
@ -123,19 +123,19 @@ public class UserTest extends AbstractClientTest {
link.setUserId("social-user-id");
link.setUserName("social-username");
Response response = user.addSocialLink("social-provider-id", link);
Response response = user.addFederatedIdentity("social-provider-id", link);
assertEquals(204, response.getStatus());
}
@Test
@Ignore("Refactor based on KEYCLOAK-883")
public void getSocialLinks() {
addSocialLink();
public void getFederatedIdentities() {
addFederatedIdentity();
UserResource user = realm.users().get("user1");
assertEquals(1, user.getSocialLinks().size());
assertEquals(1, user.getFederatedIdentity().size());
FederatedIdentityRepresentation link = user.getSocialLinks().get(0);
FederatedIdentityRepresentation link = user.getFederatedIdentity().get(0);
assertEquals("social-provider-id", link.getIdentityProvider());
assertEquals("social-user-id", link.getUserId());
assertEquals("social-username", link.getUserName());
@ -143,15 +143,15 @@ public class UserTest extends AbstractClientTest {
@Test
@Ignore("Refactor based on KEYCLOAK-883")
public void removeSocialLink() {
addSocialLink();
public void removeFederatedIdentity() {
addFederatedIdentity();
UserResource user = realm.users().get("user1");
assertEquals(1, user.getSocialLinks().size());
assertEquals(1, user.getFederatedIdentity().size());
user.removeSocialLink("social-provider-id");
user.removeFederatedIdentity("social-provider-id");
assertEquals(0, user.getSocialLinks().size());
assertEquals(0, user.getFederatedIdentity().size());
}
@Test