KEYCLOAK-828 SPNEGO/Kerberos broker - step 1

This commit is contained in:
mposolda 2015-02-09 21:40:16 +01:00
parent d1c4d9795b
commit 0c2795cf7c
12 changed files with 530 additions and 0 deletions

34
broker/kerberos/pom.xml Normal file
View file

@ -0,0 +1,34 @@
<?xml version="1.0"?>
<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>
<groupId>org.keycloak</groupId>
<version>1.2.0.Beta1-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>keycloak-broker-kerberos</artifactId>
<name>Keycloak Broker Kerberos</name>
<description/>
<packaging>jar</packaging>
<dependencies>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-broker-core</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.jboss.logging</groupId>
<artifactId>jboss-logging</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>net.iharder</groupId>
<artifactId>base64</artifactId>
<scope>provided</scope>
</dependency>
</dependencies>
</project>

View file

@ -0,0 +1,25 @@
package org.keycloak.broker.kerberos;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class KerberosConstants {
/**
* Value of HTTP Headers "WWW-Authenticate" or "Authorization" used for SPNEGO/Kerberos
**/
public static final String NEGOTIATE = "Negotiate";
/**
* Helper parameter for relay state
*/
public static final String RELAY_STATE_PARAM = "RelayState";
/**
* OID of SPNEGO mechanism. See http://www.oid-info.com/get/1.3.6.1.5.5.2
*/
public static final String SPNEGO_OID = "1.3.6.1.5.5.2";
}

View file

@ -0,0 +1,127 @@
package org.keycloak.broker.kerberos;
import java.net.URI;
import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.UriBuilder;
import javax.ws.rs.core.UriInfo;
import org.jboss.logging.Logger;
import org.keycloak.broker.kerberos.impl.KerberosServerSubjectAuthenticator;
import org.keycloak.broker.kerberos.impl.SPNEGOAuthenticator;
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.models.FederatedIdentityModel;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class KerberosIdentityProvider extends AbstractIdentityProvider<KerberosIdentityProviderConfig> {
private static final Logger logger = Logger.getLogger(KerberosIdentityProvider.class);
public KerberosIdentityProvider(KerberosIdentityProviderConfig config) {
super(config);
}
@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();
Response response = Response.status(302)
.location(redirectUri)
.build();
return AuthenticationResponse.fromResponse(response);
}
@Override
public String getRelayState(AuthenticationRequest request) {
UriInfo uriInfo = request.getUriInfo();
return uriInfo.getQueryParameters().getFirst(KerberosConstants.RELAY_STATE_PARAM);
}
@Override
public AuthenticationResponse handleResponse(AuthenticationRequest request) {
String authHeader = request.getHttpRequest().getHttpHeaders().getRequestHeaders().getFirst(HttpHeaders.AUTHORIZATION);
// Case when we don't yet have any Negotiate header
if (authHeader == null) {
return sendNegotiateResponse(null);
}
String[] tokens = authHeader.split(" ");
if (tokens.length != 2) {
logger.warn("Invalid length of tokens: " + tokens.length);
return sendNegotiateResponse(null);
} else if (!KerberosConstants.NEGOTIATE.equalsIgnoreCase(tokens[0])) {
logger.warn("Unknown scheme " + tokens[0]);
return sendNegotiateResponse(null);
} else {
String spnegoToken = tokens[1];
SPNEGOAuthenticator spnegoAuthenticator = createSPNEGOAuthenticator(spnegoToken);
spnegoAuthenticator.authenticate();
if (spnegoAuthenticator.isAuthenticated()) {
FederatedIdentity federatedIdentity = getFederatedIdentity(spnegoAuthenticator);
return AuthenticationResponse.end(federatedIdentity);
} else {
return sendNegotiateResponse(spnegoAuthenticator.getResponseToken());
}
}
}
protected SPNEGOAuthenticator createSPNEGOAuthenticator(String spnegoToken) {
KerberosServerSubjectAuthenticator kerberosAuth = createKerberosSubjectAuthenticator();
return new SPNEGOAuthenticator(kerberosAuth, spnegoToken);
}
protected KerberosServerSubjectAuthenticator createKerberosSubjectAuthenticator() {
return new KerberosServerSubjectAuthenticator(getConfig());
}
/**
* Send response with header "WWW-Authenticate: Negotiate {negotiateToken}"
*
* @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) {
String negotiateHeader = negotiateToken == null ? KerberosConstants.NEGOTIATE : KerberosConstants.NEGOTIATE + " " + negotiateToken;
Response response = Response.status(Response.Status.UNAUTHORIZED)
.header(HttpHeaders.WWW_AUTHENTICATE, negotiateHeader)
.build();
return AuthenticationResponse.fromResponse(response);
}
protected FederatedIdentity getFederatedIdentity(SPNEGOAuthenticator spnegoAuthenticator) {
String kerberosUsername = spnegoAuthenticator.getPrincipal();
FederatedIdentity user = new FederatedIdentity(kerberosUsername);
user.setUsername(kerberosUsername);
// Just guessing email, but likely can't do anything better...
String[] tokens = kerberosUsername.split("@");
String email = tokens[0] + "@" + tokens[1].toLowerCase();
user.setEmail(email);
return user;
}
@Override
public Response retrieveToken(FederatedIdentityModel identity) {
logger.warn("retrieveToken unsupported for Kerberos right now");
return null;
}
}

View file

@ -0,0 +1,26 @@
package org.keycloak.broker.kerberos;
import org.keycloak.models.IdentityProviderModel;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class KerberosIdentityProviderConfig extends IdentityProviderModel {
public KerberosIdentityProviderConfig(IdentityProviderModel identityProviderModel) {
super(identityProviderModel);
}
public String getServerPrincipal() {
return getConfig().get("serverPrincipal");
}
public String getKeyTab() {
return getConfig().get("keyTab");
}
public boolean getDebug() {
return Boolean.valueOf(getConfig().get("debug"));
}
}

View file

@ -0,0 +1,28 @@
package org.keycloak.broker.kerberos;
import org.keycloak.broker.kerberos.KerberosIdentityProvider;
import org.keycloak.broker.provider.AbstractIdentityProviderFactory;
import org.keycloak.models.IdentityProviderModel;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class KerberosIdentityProviderFactory extends AbstractIdentityProviderFactory<KerberosIdentityProvider> {
public static final String PROVIDER_ID = "kerberos";
@Override
public String getId() {
return PROVIDER_ID;
}
@Override
public String getName() {
return "Kerberos";
}
@Override
public KerberosIdentityProvider create(IdentityProviderModel model) {
return new KerberosIdentityProvider(new KerberosIdentityProviderConfig(model));
}
}

View file

@ -0,0 +1,66 @@
package org.keycloak.broker.kerberos.impl;
import java.util.HashMap;
import java.util.Map;
import javax.security.auth.Subject;
import javax.security.auth.login.AppConfigurationEntry;
import javax.security.auth.login.Configuration;
import javax.security.auth.login.LoginContext;
import javax.security.auth.login.LoginException;
import org.jboss.logging.Logger;
import org.keycloak.broker.kerberos.KerberosIdentityProviderConfig;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class KerberosServerSubjectAuthenticator {
private static final Logger logger = Logger.getLogger(KerberosServerSubjectAuthenticator.class);
private final KerberosIdentityProviderConfig config;
private LoginContext loginContext;
public KerberosServerSubjectAuthenticator(KerberosIdentityProviderConfig config) {
this.config = config;
}
public Subject authenticateServerSubject() throws LoginException {
Configuration config = createJaasConfiguration();
loginContext = new LoginContext("does-not-matter", null, null, config);
loginContext.login();
return loginContext.getSubject();
}
public void logoutServerSubject() {
if (loginContext != null) {
try {
loginContext.logout();
} catch (LoginException le) {
logger.error("Failed to logout kerberos server subject: " + config.getServerPrincipal(), le);
}
}
}
protected Configuration createJaasConfiguration() {
return new Configuration() {
@Override
public AppConfigurationEntry[] getAppConfigurationEntry(String name) {
Map<String, Object> options = new HashMap<String, Object>();
options.put("storeKey", "true");
options.put("doNotPrompt", "true");
options.put("isInitiator", "false");
options.put("useKeyTab", "true");
options.put("keyTab", config.getKeyTab());
options.put("principal", config.getServerPrincipal());
options.put("debug", String.valueOf(config.getDebug()));
AppConfigurationEntry kerberosLMConfiguration = new AppConfigurationEntry("com.sun.security.auth.module.Krb5LoginModule", AppConfigurationEntry.LoginModuleControlFlag.REQUIRED, options);
return new AppConfigurationEntry[] { kerberosLMConfiguration };
}
};
}
}

View file

@ -0,0 +1,138 @@
package org.keycloak.broker.kerberos.impl;
import java.io.IOException;
import java.security.PrivilegedExceptionAction;
import javax.security.auth.Subject;
import net.iharder.Base64;
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 GSSManager GSS_MANAGER = GSSManager.getInstance();
private final KerberosServerSubjectAuthenticator kerberosSubjectAuthenticator;
private final String spnegoToken;
private boolean authenticated = false;
private String principal = null;
private GSSCredential delegationCredential;
private String responseToken = null;
public SPNEGOAuthenticator(KerberosServerSubjectAuthenticator kerberosSubjectAuthenticator, String spnegoToken) {
this.kerberosSubjectAuthenticator = kerberosSubjectAuthenticator;
this.spnegoToken = spnegoToken;
}
public void authenticate() {
// TODO: debug
logger.info("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);
}
} finally {
kerberosSubjectAuthenticator.logoutServerSubject();
}
}
public boolean isAuthenticated() {
return authenticated;
}
public String getPrincipal() {
return principal;
}
public String getResponseToken() {
return responseToken;
}
public GSSCredential getDelegationCredential() {
return delegationCredential;
}
private class AcceptSecContext implements PrivilegedExceptionAction<Boolean> {
@Override
public Boolean run() throws Exception {
GSSContext gssContext = null;
try {
// TODO: debug
logger.info("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();
return true;
} else {
return false;
}
} finally {
if (gssContext != null) {
gssContext.dispose();
}
}
}
}
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);
byte[] inputToken = Base64.decode(spnegoToken);
byte[] respToken = gssContext.acceptSecContext(inputToken, 0, inputToken.length);
responseToken = Base64.encodeBytes(respToken);
return gssContext;
}
protected void logAuthDetails(GSSContext gssContext) throws GSSException {
// TODO: debug
if (logger.isInfoEnabled()) {
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())
.toString();
logger.info(message);
}
}
}

View file

@ -0,0 +1 @@
org.keycloak.broker.kerberos.KerberosIdentityProviderFactory

View file

@ -18,6 +18,7 @@
<module>core</module> <module>core</module>
<module>oidc</module> <module>oidc</module>
<module>saml</module> <module>saml</module>
<module>kerberos</module>
</modules> </modules>
</project> </project>

View file

@ -93,6 +93,11 @@
<artifactId>keycloak-broker-saml</artifactId> <artifactId>keycloak-broker-saml</artifactId>
<version>${project.version}</version> <version>${project.version}</version>
</dependency> </dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-broker-kerberos</artifactId>
<version>${project.version}</version>
</dependency>
<dependency> <dependency>
<groupId>org.keycloak</groupId> <groupId>org.keycloak</groupId>
<artifactId>keycloak-social-github</artifactId> <artifactId>keycloak-social-github</artifactId>

View file

@ -785,6 +785,14 @@ module.controller('RealmIdentityProviderCtrl', function($scope, $filter, $upload
$scope.identityProvider.config.postBindingResponse = true; $scope.identityProvider.config.postBindingResponse = true;
} }
} }
$scope.initKerberosProvider = function() {
if (instance && instance.id) {
$scope.identityProvider.config.debug = $scope.getBoolean($scope.identityProvider.config.debug);
} else {
$scope.identityProvider.config.debug = false;
}
}
}); });
module.controller('RealmTokenDetailCtrl', function($scope, Realm, realm, $http, $location, $route, Dialog, Notifications, TimeUnit) { module.controller('RealmTokenDetailCtrl', function($scope, Realm, realm, $http, $location, $route, Dialog, Notifications, TimeUnit) {

View file

@ -0,0 +1,71 @@
<div class="bs-sidebar col-sm-3 " data-ng-include data-src="'partials/realm-menu.html'"></div>
<div id="content-area" class="col-sm-9" role="main" data-ng-init="initKerberosProvider()">
<data-kc-navigation data-kc-current="social" data-kc-realm="realm.realm" data-kc-social="realm.social"></data-kc-navigation>
<h2></h2>
<div id="content">
<ol class="breadcrumb">
<li><a href="#/realms/{{realm.realm}}/identity-provider-settings">Identity Providers</a></li>
<li class="active">{{identityProvider.name}} Provider Settings</li>
</ol>
<h2 class="pull-left">{{identityProvider.name}} Provider Settings</h2>
<p class="subtitle"><span class="required">*</span> Required fields</p>
<form class="form-horizontal" name="realmForm" novalidate>
<fieldset>
<div class="form-group clearfix">
<label class="col-sm-2 control-label" for="identifier">Alias <span class="required">*</span></label>
<div class="col-sm-4">
<input class="form-control" id="identifier" type="text" ng-model="identityProvider.id" required>
</div>
<span tooltip-placement="right" tooltip="The alias unique identifies an identity provider and it is also used to build the redirect uri." class="fa fa-info-circle"></span>
</div>
<div class="form-group clearfix">
<label class="col-sm-2 control-label" for="name">Name <span class="required">*</span></label>
<div class="col-sm-4">
<input class="form-control" id="name" type="text" ng-model="identityProvider.name" required>
</div>
<span tooltip-placement="right" tooltip="The friendly name for this identity provider." class="fa fa-info-circle"></span>
</div>
<div class="form-group clearfix">
<label class="col-sm-2 control-label" for="serverPrincipal">Server principal <span class="required">*</span></label>
<div class="col-sm-4">
<input class="form-control" id="serverPrincipal" type="text" ng-model="identityProvider.config.serverPrincipal" required>
</div>
<span tooltip-placement="right" tooltip="Full name of server principal for HTTP service including server and domain name. For example HTTP/host.foo.org@FOO.ORG" class="fa fa-info-circle"></span>
</div>
<div class="form-group clearfix">
<label class="col-sm-2 control-label" for="keyTab">KeyTab <span class="required">*</span></label>
<div class="col-sm-4">
<input class="form-control" id="keyTab" type="text" ng-model="identityProvider.config.keyTab" required>
</div>
<span tooltip-placement="right" tooltip="Location of Kerberos KeyTab file containing the credentials of server principal. For example /etc/krb5.keytab" class="fa fa-info-circle"></span>
</div>
<div class="form-group">
<label class="col-sm-2 control-label" for="debug">Debug </label>
<div class="col-sm-4">
<input ng-model="identityProvider.config.debug" id="debug" onoffswitch />
</div>
<span tooltip-placement="right" tooltip="Enable/disable debug logging to standard output for Krb5LoginModule." class="fa fa-info-circle"></span>
</div>
<div class="form-group">
<label class="col-sm-2 control-label" for="enabled">Enabled</label>
<div class="col-sm-4">
<input ng-model="identityProvider.enabled" id="enabled" onoffswitch />
</div>
<span tooltip-placement="right" tooltip="Enable/disable this identity provider." class="fa fa-info-circle"></span>
</div>
<div class="form-group">
<label class="col-sm-2 control-label" for="updateProfileFirstLogin">Update Profile on First Login</label>
<div class="col-sm-4">
<input ng-model="identityProvider.updateProfileFirstLogin" name="identityProvider.updateProfileFirstLogin" id="updateProfileFirstLogin" onoffswitch />
</div>
<span tooltip-placement="right" tooltip="Indicates if user must update his profile right after the first login." class="fa fa-info-circle"></span>
</div>
</fieldset>
<div class="pull-right form-actions">
<button kc-save>Save</button>
<button kc-delete data-ng-click="remove()" data-ng-show="!newIdentityProvider">Delete</button>
</div>
</form>
</div>
</div>