1
0
mirror of synced 2026-05-22 21:33:16 +00:00

Add Spring Security Kerberos

Move the Spring Security Kerberos Extension into Spring Security

Closes gh-17879
This commit is contained in:
Rob Winch
2025-09-12 14:23:19 -05:00
parent e8bf470582
commit f5fb127c8c
69 changed files with 6173 additions and 0 deletions
@@ -0,0 +1,23 @@
plugins {
id 'io.spring.convention.spring-module'
}
description = 'Spring Security Kerberos Client'
dependencies {
management platform(project(":spring-security-dependencies"))
implementation project(':spring-security-kerberos-core')
implementation project(':spring-security-kerberos-web')
api('org.springframework:spring-web')
api libs.org.apache.httpcomponents.httpclient
optional project(':spring-security-ldap')
testImplementation project(':spring-security-kerberos-test')
testImplementation 'org.springframework:spring-test'
testImplementation project(':spring-security-config')
testImplementation 'org.junit.jupiter:junit-jupiter'
testImplementation 'org.mockito:mockito-junit-jupiter'
testImplementation libs.org.assertj.assertj.core
testImplementation 'com.squareup.okhttp3:mockwebserver'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
}
@@ -0,0 +1,355 @@
/*
* Copyright 2004-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.kerberos.client;
import java.io.IOException;
import java.net.URI;
import java.security.Principal;
import java.security.PrivilegedAction;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import javax.security.auth.Subject;
import javax.security.auth.callback.Callback;
import javax.security.auth.callback.CallbackHandler;
import javax.security.auth.callback.NameCallback;
import javax.security.auth.callback.PasswordCallback;
import javax.security.auth.callback.UnsupportedCallbackException;
import javax.security.auth.kerberos.KerberosPrincipal;
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.apache.hc.client5.http.SystemDefaultDnsResolver;
import org.apache.hc.client5.http.auth.AuthSchemeFactory;
import org.apache.hc.client5.http.auth.AuthScope;
import org.apache.hc.client5.http.auth.Credentials;
import org.apache.hc.client5.http.auth.KerberosConfig;
import org.apache.hc.client5.http.auth.StandardAuthScheme;
import org.apache.hc.client5.http.classic.HttpClient;
import org.apache.hc.client5.http.config.RequestConfig;
import org.apache.hc.client5.http.impl.auth.BasicCredentialsProvider;
import org.apache.hc.client5.http.impl.auth.SPNegoSchemeFactory;
import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
import org.apache.hc.client5.http.impl.classic.HttpClientBuilder;
import org.apache.hc.core5.http.config.Lookup;
import org.apache.hc.core5.http.config.RegistryBuilder;
import org.springframework.http.HttpMethod;
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
import org.springframework.util.StringUtils;
import org.springframework.web.client.RequestCallback;
import org.springframework.web.client.ResponseExtractor;
import org.springframework.web.client.RestClientException;
import org.springframework.web.client.RestTemplate;
/**
* {@code RestTemplate} that is able to make kerberos SPNEGO authenticated REST requests.
* Under a hood this {@code KerberosRestTemplate} is using {@link HttpClient} to support
* Kerberos.
*
* <p>
* Generally this template can be configured in few different ways.
* <ul>
* <li>Leave keyTabLocation and userPrincipal empty if you want to use cached ticket</li>
* <li>Use keyTabLocation and userPrincipal if you want to use keytab file</li>
* <li>Use userPrincipal and password if you want to use user/password</li>
* <li>Use loginOptions if you want to customise Krb5LoginModule options</li>
* <li>Use a customised httpClient</li>
* </ul>
*
* @author Janne Valkealahti
*
*/
public class KerberosRestTemplate extends RestTemplate {
private static final Credentials credentials = new NullCredentials();
private final String keyTabLocation;
private final String userPrincipal;
private final String password;
private final Map<String, Object> loginOptions;
/**
* Instantiates a new kerberos rest template.
*/
public KerberosRestTemplate() {
this(null, null, null, null, buildHttpClient());
}
/**
* Instantiates a new kerberos rest template.
* @param httpClient the http client
*/
public KerberosRestTemplate(HttpClient httpClient) {
this(null, null, null, null, httpClient);
}
/**
* Instantiates a new kerberos rest template.
* @param keyTabLocation the key tab location
* @param userPrincipal the user principal
*/
public KerberosRestTemplate(String keyTabLocation, String userPrincipal) {
this(keyTabLocation, userPrincipal, buildHttpClient());
}
/**
* Instantiates a new kerberos rest template.
* @param keyTabLocation the key tab location
* @param userPrincipal the user principal
* @param httpClient the http client
*/
public KerberosRestTemplate(String keyTabLocation, String userPrincipal, HttpClient httpClient) {
this(keyTabLocation, userPrincipal, null, null, httpClient);
}
/**
* Instantiates a new kerberos rest template.
* @param loginOptions the login options
*/
public KerberosRestTemplate(Map<String, Object> loginOptions) {
this(null, null, null, loginOptions, buildHttpClient());
}
/**
* Instantiates a new kerberos rest template.
* @param loginOptions the login options
* @param httpClient the http client
*/
public KerberosRestTemplate(Map<String, Object> loginOptions, HttpClient httpClient) {
this(null, null, null, loginOptions, httpClient);
}
/**
* Instantiates a new kerberos rest template.
* @param keyTabLocation the key tab location
* @param userPrincipal the user principal
* @param loginOptions the login options
*/
public KerberosRestTemplate(String keyTabLocation, String userPrincipal, Map<String, Object> loginOptions) {
this(keyTabLocation, userPrincipal, null, loginOptions, buildHttpClient());
}
/**
* Instantiates a new kerberos rest template.
* @param keyTabLocation the key tab location
* @param userPrincipal the user principal
* @param password the password
* @param loginOptions the login options
*/
public KerberosRestTemplate(String keyTabLocation, String userPrincipal, String password,
Map<String, Object> loginOptions) {
this(keyTabLocation, userPrincipal, password, loginOptions, buildHttpClient());
}
/**
* Instantiates a new kerberos rest template.
* @param keyTabLocation the key tab location
* @param userPrincipal the user principal
* @param password the password
* @param loginOptions the login options
* @param httpClient the http client
*/
private KerberosRestTemplate(String keyTabLocation, String userPrincipal, String password,
Map<String, Object> loginOptions, HttpClient httpClient) {
super(new HttpComponentsClientHttpRequestFactory(httpClient));
this.keyTabLocation = keyTabLocation;
this.userPrincipal = userPrincipal;
this.password = password;
this.loginOptions = loginOptions;
}
/**
* Builds the default instance of {@link HttpClient} having kerberos support.
* @return the http client with spneno auth scheme
*/
private static HttpClient buildHttpClient() {
HttpClientBuilder builder = HttpClientBuilder.create();
Lookup<AuthSchemeFactory> authSchemeRegistry = RegistryBuilder.<AuthSchemeFactory>create()
.register(StandardAuthScheme.SPNEGO,
new SPNegoSchemeFactory(KerberosConfig.custom()
.setStripPort(KerberosConfig.Option.ENABLE)
.setUseCanonicalHostname(KerberosConfig.Option.DISABLE)
.build(), SystemDefaultDnsResolver.INSTANCE))
.build();
builder.setDefaultAuthSchemeRegistry(authSchemeRegistry);
RequestConfig negotiate = RequestConfig.copy(RequestConfig.DEFAULT)
.setTargetPreferredAuthSchemes(Set.of(StandardAuthScheme.SPNEGO, StandardAuthScheme.KERBEROS))
.build();
builder.setDefaultRequestConfig(negotiate);
BasicCredentialsProvider credentialsProvider = new BasicCredentialsProvider();
credentialsProvider.setCredentials(new AuthScope(null, -1), credentials);
builder.setDefaultCredentialsProvider(credentialsProvider);
CloseableHttpClient httpClient = builder.build();
return httpClient;
}
/**
* Setup the {@link LoginContext} with credentials and options for authentication
* against kerberos.
* @return the login context
*/
private LoginContext buildLoginContext() throws LoginException {
ClientLoginConfig loginConfig = new ClientLoginConfig(this.keyTabLocation, this.userPrincipal, this.password,
this.loginOptions);
Set<Principal> princ = new HashSet<Principal>(1);
if (this.userPrincipal != null) {
princ.add(new KerberosPrincipal(this.userPrincipal));
}
Subject sub = new Subject(false, princ, new HashSet<Object>(), new HashSet<Object>());
CallbackHandler callbackHandler = new CallbackHandlerImpl(this.userPrincipal, this.password);
LoginContext lc = new LoginContext("", sub, callbackHandler, loginConfig);
return lc;
}
@Override
protected final <T> T doExecute(final URI url, final String uriTemplate, final HttpMethod method,
final RequestCallback requestCallback, final ResponseExtractor<T> responseExtractor)
throws RestClientException {
try {
LoginContext lc = buildLoginContext();
lc.login();
Subject serviceSubject = lc.getSubject();
return Subject.doAs(serviceSubject, new PrivilegedAction<T>() {
@Override
public T run() {
return KerberosRestTemplate.this.doExecuteSubject(url, uriTemplate, method, requestCallback,
responseExtractor);
}
});
}
catch (Exception ex) {
throw new RestClientException("Error running rest call", ex);
}
}
private <T> T doExecuteSubject(URI url, String uriTemplate, HttpMethod method, RequestCallback requestCallback,
ResponseExtractor<T> responseExtractor) throws RestClientException {
return super.doExecute(url, uriTemplate, method, requestCallback, responseExtractor);
}
private static final class ClientLoginConfig extends Configuration {
private final String keyTabLocation;
private final String userPrincipal;
private final String password;
private final Map<String, Object> loginOptions;
private ClientLoginConfig(String keyTabLocation, String userPrincipal, String password,
Map<String, Object> loginOptions) {
super();
this.keyTabLocation = keyTabLocation;
this.userPrincipal = userPrincipal;
this.password = password;
this.loginOptions = loginOptions;
}
@Override
public AppConfigurationEntry[] getAppConfigurationEntry(String name) {
Map<String, Object> options = new HashMap<String, Object>();
// if we don't have keytab or principal only option is to rely on
// credentials cache.
if (!StringUtils.hasText(this.keyTabLocation) || !StringUtils.hasText(this.userPrincipal)) {
// cache
options.put("useTicketCache", "true");
}
else {
// keytab
options.put("useKeyTab", "true");
options.put("keyTab", this.keyTabLocation);
options.put("principal", this.userPrincipal);
options.put("storeKey", "true");
}
options.put("doNotPrompt", Boolean.toString(this.password == null));
options.put("isInitiator", "true");
if (this.loginOptions != null) {
options.putAll(this.loginOptions);
}
return new AppConfigurationEntry[] {
new AppConfigurationEntry("com.sun.security.auth.module.Krb5LoginModule",
AppConfigurationEntry.LoginModuleControlFlag.REQUIRED, options) };
}
}
private static class NullCredentials implements Credentials {
@Override
public Principal getUserPrincipal() {
return null;
}
@Override
public char[] getPassword() {
return null;
}
}
private static final class CallbackHandlerImpl implements CallbackHandler {
private final String userPrincipal;
private final String password;
private CallbackHandlerImpl(String userPrincipal, String password) {
super();
this.userPrincipal = userPrincipal;
this.password = password;
}
@Override
public void handle(Callback[] callbacks) throws IOException, UnsupportedCallbackException {
for (Callback callback : callbacks) {
if (callback instanceof NameCallback) {
NameCallback nc = (NameCallback) callback;
nc.setName(this.userPrincipal);
}
else if (callback instanceof PasswordCallback) {
PasswordCallback pc = (PasswordCallback) callback;
pc.setPassword(this.password.toCharArray());
}
else {
throw new UnsupportedCallbackException(callback, "Unknown Callback");
}
}
}
}
}
@@ -0,0 +1,122 @@
/*
* Copyright 2004-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.kerberos.client.config;
import java.util.HashMap;
import javax.security.auth.login.AppConfigurationEntry;
import javax.security.auth.login.Configuration;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;
import org.springframework.util.Assert;
/**
* Implementation of {@link Configuration} which uses Sun's JAAS Krb5LoginModule.
*
* @author Nelson Rodrigues
* @author Janne Valkealahti
*
*/
public class SunJaasKrb5LoginConfig extends Configuration implements InitializingBean {
private static final Log LOG = LogFactory.getLog(SunJaasKrb5LoginConfig.class);
private String servicePrincipal;
private Resource keyTabLocation;
private Boolean useTicketCache = false;
private Boolean isInitiator = false;
private Boolean debug = false;
private String keyTabLocationAsString;
public void setServicePrincipal(String servicePrincipal) {
this.servicePrincipal = servicePrincipal;
}
public void setKeyTabLocation(Resource keyTabLocation) {
this.keyTabLocation = keyTabLocation;
}
public void setUseTicketCache(Boolean useTicketCache) {
this.useTicketCache = useTicketCache;
}
public void setIsInitiator(Boolean isInitiator) {
this.isInitiator = isInitiator;
}
public void setDebug(Boolean debug) {
this.debug = debug;
}
@Override
public void afterPropertiesSet() throws Exception {
Assert.hasText(this.servicePrincipal, "servicePrincipal must be specified");
if (this.keyTabLocation != null && this.keyTabLocation instanceof ClassPathResource) {
LOG.warn(
"Your keytab is in the classpath. This file needs special protection and shouldn't be in the classpath. JAAS may also not be able to load this file from classpath.");
}
if (!this.useTicketCache) {
Assert.notNull(this.keyTabLocation, "keyTabLocation must be specified when useTicketCache is false");
}
if (this.keyTabLocation != null) {
this.keyTabLocationAsString = this.keyTabLocation.getURL().toExternalForm();
if (this.keyTabLocationAsString.startsWith("file:")) {
this.keyTabLocationAsString = this.keyTabLocationAsString.substring(5);
}
}
}
@Override
public AppConfigurationEntry[] getAppConfigurationEntry(String name) {
HashMap<String, String> options = new HashMap<>();
options.put("principal", this.servicePrincipal);
if (this.keyTabLocation != null) {
options.put("useKeyTab", "true");
options.put("keyTab", this.keyTabLocationAsString);
options.put("storeKey", "true");
}
options.put("doNotPrompt", "true");
if (this.useTicketCache) {
options.put("useTicketCache", "true");
options.put("renewTGT", "true");
}
options.put("isInitiator", this.isInitiator.toString());
options.put("debug", this.debug.toString());
return new AppConfigurationEntry[] { new AppConfigurationEntry("com.sun.security.auth.module.Krb5LoginModule",
AppConfigurationEntry.LoginModuleControlFlag.REQUIRED, options), };
}
}
@@ -0,0 +1,156 @@
/*
* Copyright 2004-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.kerberos.client.ldap;
import java.security.PrivilegedAction;
import java.util.Hashtable;
import java.util.List;
import javax.naming.AuthenticationException;
import javax.naming.Context;
import javax.naming.NamingException;
import javax.naming.directory.DirContext;
import javax.security.auth.Subject;
import javax.security.auth.login.Configuration;
import javax.security.auth.login.LoginContext;
import javax.security.auth.login.LoginException;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.ldap.core.support.LdapContextSource;
import org.springframework.security.kerberos.client.config.SunJaasKrb5LoginConfig;
import org.springframework.security.ldap.DefaultSpringSecurityContextSource;
import org.springframework.util.Assert;
/**
* Implementation of an {@link LdapContextSource} that authenticates with the ldap server
* using Kerberos.
*
* Example usage:
*
* <pre>
* &lt;bean id=&quot;authorizationContextSource&quot; class=&quot;org.springframework.security.kerberos.ldap.KerberosLdapContextSource&quot;&gt;
* &lt;constructor-arg value=&quot;${authentication.ldap.ldapUrl}&quot; /&gt;
* &lt;property name=&quot;referral&quot; value=&quot;ignore&quot; /&gt;
*
* &lt;property name=&quot;loginConfig&quot;&gt;
* &lt;bean class=&quot;org.springframework.security.kerberos.client.config.SunJaasKrb5LoginConfig&quot;&gt;
* &lt;property name=&quot;servicePrincipal&quot; value=&quot;${authentication.ldap.servicePrincipal}&quot; /&gt;
* &lt;property name=&quot;useTicketCache&quot; value=&quot;true&quot; /&gt;
* &lt;property name=&quot;isInitiator&quot; value=&quot;true&quot; /&gt;
* &lt;property name=&quot;debug&quot; value=&quot;false&quot; /&gt;
* &lt;/bean&gt;
* &lt;/property&gt;
* &lt;/bean&gt;
*
* &lt;sec:ldap-user-service id=&quot;ldapUserService&quot; server-ref=&quot;authorizationContextSource&quot; user-search-filter=&quot;(| (userPrincipalName={0}) (sAMAccountName={0}))&quot;
* group-search-filter=&quot;(member={0})&quot; group-role-attribute=&quot;cn&quot; role-prefix=&quot;none&quot; /&gt;
* </pre>
*
* @author Nelson Rodrigues
* @see SunJaasKrb5LoginConfig
*/
public class KerberosLdapContextSource extends DefaultSpringSecurityContextSource implements InitializingBean {
private Configuration loginConfig;
/**
* Instantiates a new kerberos ldap context source.
* @param url the url
*/
public KerberosLdapContextSource(String url) {
super(url);
}
/**
* Instantiates a new kerberos ldap context source.
* @param urls the urls
* @param baseDn the base dn
*/
public KerberosLdapContextSource(List<String> urls, String baseDn) {
super(urls, baseDn);
}
@Override
public void afterPropertiesSet() /* throws Exception */ {
// org.springframework.ldap.core.support.AbstractContextSource in 4.x
// doesn't throw Exception for its InitializingBean method, so
// we had to remove it from here also. Addition to that
// we need to catch super call and re-throw.
try {
super.afterPropertiesSet();
}
catch (Exception ex) {
throw new RuntimeException(ex);
}
Assert.notNull(this.loginConfig, "loginConfig must be specified");
}
@SuppressWarnings("unchecked")
@Override
protected DirContext getDirContextInstance(final @SuppressWarnings("rawtypes") Hashtable environment)
throws NamingException {
environment.put(Context.SECURITY_AUTHENTICATION, "GSSAPI");
Subject serviceSubject = login();
final NamingException[] suppressedException = new NamingException[] { null };
DirContext dirContext = Subject.doAs(serviceSubject, new PrivilegedAction<>() {
@Override
public DirContext run() {
try {
return KerberosLdapContextSource.super.getDirContextInstance(environment);
}
catch (NamingException ex) {
suppressedException[0] = ex;
return null;
}
}
});
if (suppressedException[0] != null) {
throw suppressedException[0];
}
return dirContext;
}
/**
* The login configuration to get the serviceSubject from LoginContext
* @param loginConfig the login config
*/
public void setLoginConfig(Configuration loginConfig) {
this.loginConfig = loginConfig;
}
private Subject login() throws AuthenticationException {
try {
LoginContext lc = new LoginContext(KerberosLdapContextSource.class.getSimpleName(), null, null,
this.loginConfig);
lc.login();
return lc.getSubject();
}
catch (LoginException ex) {
AuthenticationException ae = new AuthenticationException(ex.getMessage());
ae.initCause(ex);
throw ae;
}
}
}
@@ -0,0 +1,135 @@
/*
* Copyright 2004-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.kerberos.client;
import java.io.File;
import java.nio.charset.StandardCharsets;
import java.util.Collections;
import okhttp3.mockwebserver.Dispatcher;
import okhttp3.mockwebserver.MockResponse;
import okhttp3.mockwebserver.MockWebServer;
import okhttp3.mockwebserver.RecordedRequest;
import okio.Buffer;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.security.kerberos.test.KerberosSecurityTestcase;
import org.springframework.security.kerberos.test.MiniKdc;
import static org.assertj.core.api.Assertions.assertThat;
class KerberosRestTemplateTests extends KerberosSecurityTestcase {
private final MockWebServer server = new MockWebServer();
private static final String helloWorld = "Hello World";
private static final MediaType textContentType = new MediaType("text", "plain",
Collections.singletonMap("charset", "UTF-8"));
private int port;
private String baseUrl;
private KerberosRestTemplate restTemplate;
private String clientPrincipal;
private File clientKeytab;
@BeforeEach
void setUp() throws Exception {
this.server.setDispatcher(new TestDispatcher());
this.server.start();
this.port = this.server.getPort();
this.baseUrl = "http://localhost:" + this.port;
MiniKdc kdc = getKdc();
File workDir = getWorkDir();
this.clientPrincipal = "client/localhost";
this.clientKeytab = new File(workDir, "client.keytab");
kdc.createPrincipal(this.clientKeytab, this.clientPrincipal);
String serverPrincipal = "HTTP/localhost";
File serverKeytab = new File(workDir, "server.keytab");
kdc.createPrincipal(serverKeytab, serverPrincipal);
}
@AfterEach
void tearDown() throws Exception {
this.server.shutdown();
}
@Test
void sendsNegotiateHeader() {
setUpClient();
String s = this.restTemplate.getForObject(this.baseUrl + "/get", String.class);
assertThat(s).isEqualTo(helloWorld);
}
private void setUpClient() {
this.restTemplate = new KerberosRestTemplate(this.clientKeytab.getAbsolutePath(), this.clientPrincipal);
}
private MockResponse getRequest(RecordedRequest request, byte[] body, String contentType) {
if (request.getMethod().equals("OPTIONS")) {
return new MockResponse().setResponseCode(200).setHeader("Allow", "GET, OPTIONS, HEAD, TRACE");
}
Buffer buf = new Buffer();
buf.write(body);
MockResponse response = new MockResponse().setHeader(HttpHeaders.CONTENT_LENGTH, body.length)
.setBody(buf)
.setResponseCode(200);
if (contentType != null) {
response = response.setHeader(HttpHeaders.CONTENT_TYPE, contentType);
}
return response;
}
protected class TestDispatcher extends Dispatcher {
@Override
public MockResponse dispatch(RecordedRequest request) {
try {
byte[] helloWorldBytes = helloWorld.getBytes(StandardCharsets.UTF_8);
if (request.getPath().equals("/get")) {
String header = request.getHeader(HttpHeaders.AUTHORIZATION);
if (header == null) {
return new MockResponse().setResponseCode(401)
.addHeader(HttpHeaders.WWW_AUTHENTICATE, "Negotiate");
}
else if (header.startsWith("Negotiate ")) {
return getRequest(request, helloWorldBytes, textContentType.toString());
}
}
return new MockResponse().setResponseCode(404);
}
catch (Throwable ex) {
return new MockResponse().setResponseCode(500).setBody(ex.toString());
}
}
}
}
@@ -0,0 +1,10 @@
log4j.rootCategory=INFO, stdout
log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern=%d{ABSOLUTE} %5p %t %c{2} - %m%n
log4j.category.org.springframework.boot=INFO
xlog4j.category.org.apache.http.wire=TRACE
xlog4j.category.org.apache.http.headers=TRACE
@@ -0,0 +1,26 @@
#
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
[libdefaults]
default_realm = {0}
udp_preference_limit = 1
forwardable = true
[realms]
{0} = '{'
kdc = {1}:{2}
'}'
@@ -0,0 +1,86 @@
#
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
dn: ou=users,dc=${0},dc=${1}
objectClass: organizationalUnit
objectClass: top
ou: users
dn: uid=krbtgt,ou=users,dc=${0},dc=${1}
objectClass: top
objectClass: person
objectClass: inetOrgPerson
objectClass: krb5principal
objectClass: krb5kdcentry
cn: KDC Service
sn: Service
uid: krbtgt
userPassword: secret
krb5PrincipalName: krbtgt/${2}.${3}@${2}.${3}
krb5KeyVersionNumber: 0
dn: uid=ldap,ou=users,dc=${0},dc=${1}
objectClass: top
objectClass: person
objectClass: inetOrgPerson
objectClass: krb5principal
objectClass: krb5kdcentry
cn: LDAP
sn: Service
uid: ldap
userPassword: secret
krb5PrincipalName: ldap/${4}@${2}.${3}
krb5KeyVersionNumber: 0
dn: uid=user1,ou=users,dc=${0},dc=${1}
objectClass: top
objectClass: person
objectClass: inetOrgPerson
objectClass: krb5principal
objectClass: krb5kdcentry
cn: user1
sn: Service
uid: user1
userPassword: secret
krb5PrincipalName: user1@${2}.${3}
krb5KeyVersionNumber: 0
dn: uid=webtier,ou=users,dc=${0},dc=${1}
objectClass: top
objectClass: person
objectClass: inetOrgPerson
objectClass: krb5principal
objectClass: krb5kdcentry
cn: webtier
sn: Service
uid: webtier
userPassword: secret
krb5PrincipalName: HTTP/webtier@${2}.${3}
krb5KeyVersionNumber: 0
dn: uid=servicetier,ou=users,dc=${0},dc=${1}
objectClass: top
objectClass: person
objectClass: inetOrgPerson
objectClass: krb5principal
objectClass: krb5kdcentry
cn: servicetier
sn: Service
uid: servicetier
userPassword: secret
krb5PrincipalName: HTTP/servicetier@${2}.${3}
krb5KeyVersionNumber: 0
@@ -0,0 +1,15 @@
plugins {
id 'io.spring.convention.spring-module'
}
description = 'Spring Security Kerberos Core'
dependencies {
management platform(project(":spring-security-dependencies"))
api(project(':spring-security-core'))
testImplementation 'org.junit.jupiter:junit-jupiter'
testImplementation 'org.mockito:mockito-junit-jupiter'
testImplementation libs.org.assertj.assertj.core
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
}
@@ -0,0 +1,72 @@
/*
* Copyright 2004-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.kerberos.authentication;
import java.io.Serializable;
import java.util.HashMap;
import java.util.Map;
import javax.security.auth.Subject;
import org.springframework.security.kerberos.authentication.sun.SunJaasKerberosClient;
/**
* <p>
* Holds the Subject of the currently authenticated user, since this Jaas object also has
* the credentials, and permits creating new credentials against other Kerberos services.
* </p>
*
* @author Bogdan Mustiata
* @see SunJaasKerberosClient
* @see org.springframework.security.kerberos.authentication.KerberosAuthenticationProvider
*/
public class JaasSubjectHolder implements Serializable {
private static final long serialVersionUID = 8174713761131577405L;
private Subject jaasSubject;
private String username;
private Map<String, byte[]> savedTokens = new HashMap<String, byte[]>();
public JaasSubjectHolder(Subject jaasSubject) {
this.jaasSubject = jaasSubject;
}
public JaasSubjectHolder(Subject jaasSubject, String username) {
this.jaasSubject = jaasSubject;
this.username = username;
}
public String getUsername() {
return this.username;
}
public Subject getJaasSubject() {
return this.jaasSubject;
}
public void addToken(String targetService, byte[] outToken) {
this.savedTokens.put(targetService, outToken);
}
public byte[] getToken(String principalName) {
return this.savedTokens.get(principalName);
}
}
@@ -0,0 +1,23 @@
/*
* Copyright 2004-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.kerberos.authentication;
public interface KerberosAuthentication {
JaasSubjectHolder getJaasSubjectHolder();
}
@@ -0,0 +1,72 @@
/*
* Copyright 2004-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.kerberos.authentication;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
/**
* {@link AuthenticationProvider} for kerberos.
*
* @author Mike Wiesner
* @author Bogdan Mustiata
* @since 1.0
*/
public class KerberosAuthenticationProvider implements AuthenticationProvider {
private KerberosClient kerberosClient;
private UserDetailsService userDetailsService;
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
UsernamePasswordAuthenticationToken auth = (UsernamePasswordAuthenticationToken) authentication;
JaasSubjectHolder subjectHolder = this.kerberosClient.login(auth.getName(), auth.getCredentials().toString());
UserDetails userDetails = this.userDetailsService.loadUserByUsername(subjectHolder.getUsername());
KerberosUsernamePasswordAuthenticationToken output = new KerberosUsernamePasswordAuthenticationToken(
userDetails, auth.getCredentials(), userDetails.getAuthorities(), subjectHolder);
output.setDetails(authentication.getDetails());
return output;
}
@Override
public boolean supports(Class<? extends Object> authentication) {
return (UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication));
}
/**
* Sets the kerberos client.
* @param kerberosClient the new kerberos client
*/
public void setKerberosClient(KerberosClient kerberosClient) {
this.kerberosClient = kerberosClient;
}
/**
* Sets the user details service.
* @param detailsService the new user details service
*/
public void setUserDetailsService(UserDetailsService detailsService) {
this.userDetailsService = detailsService;
}
}
@@ -0,0 +1,29 @@
/*
* Copyright 2004-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.kerberos.authentication;
/**
* @author Mike Wiesner
* @author Bogdan Mustiata
* @since 1.0
* @version $Id$
*/
public interface KerberosClient {
JaasSubjectHolder login(String username, String password);
}
@@ -0,0 +1,132 @@
/*
* Copyright 2004-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.kerberos.authentication;
import java.security.PrivilegedAction;
import javax.security.auth.Subject;
import org.ietf.jgss.GSSContext;
import org.ietf.jgss.GSSCredential;
import org.ietf.jgss.GSSException;
import org.ietf.jgss.GSSManager;
import org.ietf.jgss.GSSName;
import org.ietf.jgss.Oid;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.core.Authentication;
/**
* <p>
* Allows creating tickets against other service principals storing the tickets in the
* KerberosAuthentication's JaasSubjectHolder.
* </p>
*
* @author Bogdan Mustiata
*/
public final class KerberosMultiTier {
public static final String KERBEROS_OID_STRING = "1.2.840.113554.1.2.2";
public static final Oid KERBEROS_OID = createOid(KERBEROS_OID_STRING);
/**
* Create a new ticket for the
* @param authentication
* @param username
* @param lifetimeInSeconds
* @param targetService
* @return
*/
public static Authentication authenticateService(Authentication authentication, final String username,
final int lifetimeInSeconds, final String targetService) {
KerberosAuthentication kerberosAuthentication = (KerberosAuthentication) authentication;
final JaasSubjectHolder jaasSubjectHolder = kerberosAuthentication.getJaasSubjectHolder();
Subject subject = jaasSubjectHolder.getJaasSubject();
Subject.doAs(subject, new PrivilegedAction<Object>() {
@Override
public Object run() {
runAuthentication(jaasSubjectHolder, username, lifetimeInSeconds, targetService);
return null;
}
});
return authentication;
}
public static byte[] getTokenForService(Authentication authentication, String principalName) {
KerberosAuthentication kerberosAuthentication = (KerberosAuthentication) authentication;
final JaasSubjectHolder jaasSubjectHolder = kerberosAuthentication.getJaasSubjectHolder();
return jaasSubjectHolder.getToken(principalName);
}
private static void runAuthentication(JaasSubjectHolder jaasContext, String username, int lifetimeInSeconds,
String targetService) {
try {
GSSManager manager = GSSManager.getInstance();
GSSName clientName = manager.createName(username, GSSName.NT_USER_NAME);
GSSCredential clientCredential = manager.createCredential(clientName, lifetimeInSeconds, KERBEROS_OID,
GSSCredential.INITIATE_ONLY);
GSSName serverName = manager.createName(targetService, GSSName.NT_USER_NAME);
GSSContext securityContext = manager.createContext(serverName, KERBEROS_OID, clientCredential,
GSSContext.DEFAULT_LIFETIME);
securityContext.requestCredDeleg(true);
securityContext.requestInteg(false);
securityContext.requestAnonymity(false);
securityContext.requestMutualAuth(false);
securityContext.requestReplayDet(false);
securityContext.requestSequenceDet(false);
boolean established = false;
byte[] outToken = new byte[0];
while (!established) {
byte[] inToken = new byte[0];
outToken = securityContext.initSecContext(inToken, 0, inToken.length);
established = securityContext.isEstablished();
}
jaasContext.addToken(targetService, outToken);
}
catch (Exception ex) {
throw new BadCredentialsException("Kerberos authentication failed", ex);
}
}
private static Oid createOid(String oid) {
try {
return new Oid(oid);
}
catch (GSSException ex) {
throw new IllegalStateException("Unable to instantiate Oid: ", ex);
}
}
private KerberosMultiTier() {
}
}
@@ -0,0 +1,122 @@
/*
* Copyright 2004-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.kerberos.authentication;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.security.authentication.AccountStatusUserDetailsChecker;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsChecker;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.util.Assert;
/**
* <p>
* Authentication Provider which validates Kerberos Service Tickets or SPNEGO Tokens
* (which includes Kerberos Service Tickets).
* </p>
*
* <p>
* It needs a <code>KerberosTicketValidator</code>, which contains the code to validate
* the ticket, as this code is different between SUN and IBM JRE.<br>
* It also needs an <code>UserDetailsService</code> to load the user properties and the
* <code>GrantedAuthorities</code>, as we only get back the username from Kerbeos
* </p>
*
* You can see an example configuration in
* <code>SpnegoAuthenticationProcessingFilter</code>.
*
* @author Mike Wiesner
* @author Jeremy Stone
* @since 1.0
* @see KerberosTicketValidator
* @see UserDetailsService
*/
public class KerberosServiceAuthenticationProvider implements AuthenticationProvider, InitializingBean {
private static final Log LOG = LogFactory.getLog(KerberosServiceAuthenticationProvider.class);
private KerberosTicketValidator ticketValidator;
private UserDetailsService userDetailsService;
private UserDetailsChecker userDetailsChecker = new AccountStatusUserDetailsChecker();
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
KerberosServiceRequestToken auth = (KerberosServiceRequestToken) authentication;
byte[] token = auth.getToken();
LOG.debug("Try to validate Kerberos Token");
KerberosTicketValidation ticketValidation = this.ticketValidator.validateTicket(token);
LOG.debug("Successfully validated " + ticketValidation.username());
UserDetails userDetails = this.userDetailsService.loadUserByUsername(ticketValidation.username());
this.userDetailsChecker.check(userDetails);
additionalAuthenticationChecks(userDetails, auth);
KerberosServiceRequestToken responseAuth = new KerberosServiceRequestToken(userDetails, ticketValidation,
userDetails.getAuthorities(), token);
responseAuth.setDetails(authentication.getDetails());
return responseAuth;
}
@Override
public boolean supports(Class<? extends Object> auth) {
return KerberosServiceRequestToken.class.isAssignableFrom(auth);
}
@Override
public void afterPropertiesSet() throws Exception {
Assert.notNull(this.ticketValidator, "ticketValidator must be specified");
Assert.notNull(this.userDetailsService, "userDetailsService must be specified");
}
/**
* The <code>UserDetailsService</code> to use, for loading the user properties and the
* <code>GrantedAuthorities</code>.
* @param userDetailsService the new user details service
*/
public void setUserDetailsService(UserDetailsService userDetailsService) {
this.userDetailsService = userDetailsService;
}
/**
* The <code>KerberosTicketValidator</code> to use, for validating the Kerberos/SPNEGO
* tickets.
* @param ticketValidator the new ticket validator
*/
public void setTicketValidator(KerberosTicketValidator ticketValidator) {
this.ticketValidator = ticketValidator;
}
/**
* Allows subclasses to perform any additional checks of a returned
* <code>UserDetails</code> for a given authentication request.
* @param userDetails as retrieved from the {@link UserDetailsService}
* @param authentication validated {@link KerberosServiceRequestToken}
* @throws AuthenticationException AuthenticationException if the credentials could
* not be validated (generally a <code>BadCredentialsException</code>, an
* <code>AuthenticationServiceException</code>)
*/
protected void additionalAuthenticationChecks(UserDetails userDetails, KerberosServiceRequestToken authentication)
throws AuthenticationException {
}
}
@@ -0,0 +1,233 @@
/*
* Copyright 2004-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.kerberos.authentication;
import java.security.PrivilegedActionException;
import java.security.PrivilegedExceptionAction;
import java.util.Arrays;
import java.util.Base64;
import java.util.Collection;
import javax.security.auth.Subject;
import org.ietf.jgss.GSSContext;
import org.ietf.jgss.MessageProp;
import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.userdetails.UserDetails;
/**
* <p>
* Holds the Kerberos/SPNEGO token for requesting a kerberized service and is also the
* output of <code>KerberosServiceAuthenticationProvider</code>.
* </p>
* <p>
* Will mostly be created in <code>SpnegoAuthenticationProcessingFilter</code> and
* authenticated in <code>KerberosServiceAuthenticationProvider</code>.
* </p>
*
* This token cannot be re-authenticated, as you will get a Kerberos Reply error.
*
* @author Mike Wiesner
* @author Jeremy Stone
* @author Bogdan Mustiata
* @since 1.0
* @see KerberosServiceAuthenticationProvider
*/
public class KerberosServiceRequestToken extends AbstractAuthenticationToken implements KerberosAuthentication {
private static final long serialVersionUID = 395488921064775014L;
private final byte[] token;
private final Object principal;
private final transient KerberosTicketValidation ticketValidation;
private JaasSubjectHolder jaasSubjectHolder;
/**
* Creates an authenticated token, normally used as an output of an authentication
* provider.
* @param principal the user principal (mostly of instance <code>UserDetails</code>)
* @param ticketValidation result of ticket validation
* @param authorities the authorities which are granted to the user
* @param token the Kerberos/SPNEGO token
* @see UserDetails
*/
public KerberosServiceRequestToken(Object principal, KerberosTicketValidation ticketValidation,
Collection<? extends GrantedAuthority> authorities, byte[] token) {
super(authorities);
this.token = token;
this.principal = principal;
this.ticketValidation = ticketValidation;
this.jaasSubjectHolder = new JaasSubjectHolder(ticketValidation.subject(), ticketValidation.username());
super.setAuthenticated(true);
}
/**
* Creates an unauthenticated instance which should then be authenticated by
* <code>KerberosServiceAuthenticationProvider</code>.
* @param token Kerberos/SPNEGO token
* @see KerberosServiceAuthenticationProvider
*/
public KerberosServiceRequestToken(byte[] token) {
super(AuthorityUtils.NO_AUTHORITIES);
this.token = token;
this.ticketValidation = null;
this.principal = null;
}
/**
* equals() is based only on the Kerberos token
*/
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (!super.equals(obj)) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
KerberosServiceRequestToken other = (KerberosServiceRequestToken) obj;
if (!Arrays.equals(this.token, other.token)) {
return false;
}
return true;
}
/**
* Calculates hashcode based on the Kerberos token
*/
@Override
public int hashCode() {
final int prime = 31;
int result = super.hashCode();
result = prime * result + Arrays.hashCode(this.token);
return result;
}
@Override
public Object getCredentials() {
return null;
}
@Override
public Object getPrincipal() {
return this.principal;
}
/**
* Returns the Kerberos token
* @return the token data
*/
public byte[] getToken() {
return this.token;
}
/**
* Gets the ticket validation
* @return the ticket validation (which will be null if the token is unauthenticated)
*/
public KerberosTicketValidation getTicketValidation() {
return this.ticketValidation;
}
/**
* Determines whether an authenticated token has a response token
* @return whether a response token is available
*/
public boolean hasResponseToken() {
return this.ticketValidation != null && this.ticketValidation.responseToken() != null;
}
/**
* Gets the (Base64) encoded response token assuming one is available.
* @return encoded response token
*/
public String getEncodedResponseToken() {
if (!hasResponseToken()) {
throw new IllegalStateException("Unauthenticated or no response token");
}
return Base64.getEncoder().encodeToString(this.ticketValidation.responseToken());
}
/**
* Unwraps an encrypted message using the gss context
* @param data the data
* @param offset data offset
* @param length data length
* @return the decrypted message
* @throws PrivilegedActionException if jaas throws and error
*/
public byte[] decrypt(final byte[] data, final int offset, final int length) throws PrivilegedActionException {
return Subject.doAs(getTicketValidation().subject(), new PrivilegedExceptionAction<byte[]>() {
public byte[] run() throws Exception {
final GSSContext context = getTicketValidation().getGssContext();
return context.unwrap(data, offset, length, new MessageProp(true));
}
});
}
/**
* Unwraps an encrypted message using the gss context
* @param data the data
* @return the decrypted message
* @throws PrivilegedActionException if jaas throws and error
*/
public byte[] decrypt(final byte[] data) throws PrivilegedActionException {
return decrypt(data, 0, data.length);
}
/**
* Wraps an message using the gss context
* @param data the data
* @param offset data offset
* @param length data length
* @return the encrypted message
* @throws PrivilegedActionException if jaas throws and error
*/
public byte[] encrypt(final byte[] data, final int offset, final int length) throws PrivilegedActionException {
return Subject.doAs(getTicketValidation().subject(), new PrivilegedExceptionAction<byte[]>() {
public byte[] run() throws Exception {
final GSSContext context = getTicketValidation().getGssContext();
return context.wrap(data, offset, length, new MessageProp(true));
}
});
}
/**
* Wraps an message using the gss context
* @param data the data
* @return the encrypted message
* @throws PrivilegedActionException if jaas throws and error
*/
public byte[] encrypt(final byte[] data) throws PrivilegedActionException {
return encrypt(data, 0, data.length);
}
@Override
public JaasSubjectHolder getJaasSubjectHolder() {
return this.jaasSubjectHolder;
}
}
@@ -0,0 +1,92 @@
/*
* Copyright 2004-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.kerberos.authentication;
import java.util.HashSet;
import javax.security.auth.Subject;
import javax.security.auth.kerberos.KerberosPrincipal;
import org.ietf.jgss.GSSContext;
import org.ietf.jgss.GSSCredential;
/**
* Result of ticket validation
*/
public final class KerberosTicketValidation {
private final String username;
private final Subject subject;
private final byte[] responseToken;
private final GSSContext gssContext;
private final GSSCredential delegationCredential;
public KerberosTicketValidation(String username, String servicePrincipal, byte[] responseToken,
GSSContext gssContext) {
this(username, servicePrincipal, responseToken, gssContext, null);
}
public KerberosTicketValidation(String username, String servicePrincipal, byte[] responseToken,
GSSContext gssContext, GSSCredential delegationCredential) {
final HashSet<KerberosPrincipal> princs = new HashSet<KerberosPrincipal>();
princs.add(new KerberosPrincipal(servicePrincipal));
this.username = username;
this.subject = new Subject(false, princs, new HashSet<Object>(), new HashSet<Object>());
this.responseToken = responseToken;
this.gssContext = gssContext;
this.delegationCredential = delegationCredential;
}
public KerberosTicketValidation(String username, Subject subject, byte[] responseToken, GSSContext gssContext) {
this(username, subject, responseToken, gssContext, null);
}
public KerberosTicketValidation(String username, Subject subject, byte[] responseToken, GSSContext gssContext,
GSSCredential delegationCredential) {
this.username = username;
this.subject = subject;
this.responseToken = responseToken;
this.gssContext = gssContext;
this.delegationCredential = delegationCredential;
}
public String username() {
return this.username;
}
public byte[] responseToken() {
return this.responseToken;
}
public GSSContext getGssContext() {
return this.gssContext;
}
public Subject subject() {
return this.subject;
}
public GSSCredential getDelegationCredential() {
return this.delegationCredential;
}
}
@@ -0,0 +1,40 @@
/*
* Copyright 2004-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.kerberos.authentication;
import org.springframework.security.authentication.BadCredentialsException;
/**
* Implementations of this interface are used in
* {@link KerberosServiceAuthenticationProvider} to validate a Kerberos/SPNEGO Ticket.
*
* @author Mike Wiesner
* @author Jeremy Stone
* @since 1.0
* @see KerberosServiceAuthenticationProvider
*/
public interface KerberosTicketValidator {
/**
* Validates a Kerberos/SPNEGO ticket.
* @param token Kerbeos/SPNEGO ticket
* @return authenticated kerberos principal
* @throws BadCredentialsException if the ticket is not valid
*/
KerberosTicketValidation validateTicket(byte[] token) throws BadCredentialsException;
}
@@ -0,0 +1,69 @@
/*
* Copyright 2004-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.kerberos.authentication;
import java.util.Collection;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;
/**
* <p>
* Holds the Username/Password as well as the JAAS Subject allowing multi-tier
* authentications using Kerberos.
* </p>
*
* <p>
* The JAAS Subject has in its private credentials the Kerberos tickets for generating new
* tickets against other service principals using
* <code>KerberosMultiTier.authenticateService()</code>
* </p>
*
* @author Bogdan Mustiata
* @see KerberosAuthenticationProvider
* @see KerberosMultiTier
*/
public class KerberosUsernamePasswordAuthenticationToken extends UsernamePasswordAuthenticationToken
implements KerberosAuthentication {
private static final long serialVersionUID = 6327699460703504153L;
private final JaasSubjectHolder jaasSubjectHolder;
/**
* <p>
* Creates an authentication token that holds the username and password, and the
* Subject that the user will need to create new authentication tokens against other
* services.
* </p>
* @param principal
* @param credentials
* @param authorities
* @param subjectHolder
*/
public KerberosUsernamePasswordAuthenticationToken(Object principal, Object credentials,
Collection<? extends GrantedAuthority> authorities, JaasSubjectHolder subjectHolder) {
super(principal, credentials, authorities);
this.jaasSubjectHolder = subjectHolder;
}
@Override
public JaasSubjectHolder getJaasSubjectHolder() {
return this.jaasSubjectHolder;
}
}
@@ -0,0 +1,78 @@
/*
* Copyright 2004-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.kerberos.authentication.sun;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.config.BeanPostProcessor;
/**
* Config for global jaas.
*
* @author Mike Wiesner
* @since 1.0
*/
public class GlobalSunJaasKerberosConfig implements BeanPostProcessor, InitializingBean {
private boolean debug = false;
private String krbConfLocation;
@Override
public void afterPropertiesSet() throws Exception {
if (this.debug) {
System.setProperty("sun.security.krb5.debug", "true");
}
if (this.krbConfLocation != null) {
System.setProperty("java.security.krb5.conf", this.krbConfLocation);
}
}
/**
* Enable debug logs from the Sun Kerberos Implementation. Default is false.
* @param debug true if debug should be enabled
*/
public void setDebug(boolean debug) {
this.debug = debug;
}
/**
* Kerberos config file location can be specified here.
* @param krbConfLocation the path to krb config file
*/
public void setKrbConfLocation(String krbConfLocation) {
this.krbConfLocation = krbConfLocation;
}
// The following methods are not used here. This Bean implements only
// BeanPostProcessor to ensure that it
// is created before any other bean is created, because the system properties needed
// to be set very early
// in the startup-phase, but after the BeanFactoryPostProcessing.
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
return bean;
}
@Override
public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
return bean;
}
}
@@ -0,0 +1,47 @@
/*
* Copyright 2004-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.kerberos.authentication.sun;
import java.security.Principal;
import java.util.HashSet;
import javax.security.auth.Subject;
/**
* JAAS utility functions.
*
* @author Bogdan Mustiata
*/
public final class JaasUtil {
/**
* Copy the principal and the credentials into a new Subject.
* @param subject
* @return
*/
public static Subject copySubject(Subject subject) {
Subject subjectCopy = new Subject(false, new HashSet<Principal>(subject.getPrincipals()),
new HashSet<Object>(subject.getPublicCredentials()),
new HashSet<Object>(subject.getPrivateCredentials()));
return subjectCopy;
}
private JaasUtil() {
}
}
@@ -0,0 +1,153 @@
/*
* Copyright 2004-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.kerberos.authentication.sun;
import java.io.IOException;
import java.util.HashMap;
import javax.security.auth.Subject;
import javax.security.auth.callback.Callback;
import javax.security.auth.callback.CallbackHandler;
import javax.security.auth.callback.NameCallback;
import javax.security.auth.callback.PasswordCallback;
import javax.security.auth.callback.UnsupportedCallbackException;
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.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.kerberos.authentication.JaasSubjectHolder;
import org.springframework.security.kerberos.authentication.KerberosClient;
/**
* Implementation of {@link KerberosClient} which uses the SUN JAAS login module, which is
* included in the SUN JRE, it will not work with an IBM JRE. The whole configuration is
* done in this class, no additional JAAS configuration is needed.
*
* @author Mike Wiesner
* @author Bogdan Mustiata
* @since 1.0
*/
public class SunJaasKerberosClient implements KerberosClient {
private boolean debug = false;
private boolean multiTier = false;
private static final Log LOG = LogFactory.getLog(SunJaasKerberosClient.class);
@Override
public JaasSubjectHolder login(String username, String password) {
LOG.debug("Trying to authenticate " + username + " with Kerberos");
JaasSubjectHolder result;
try {
LoginContext loginContext = new LoginContext("", null,
new KerberosClientCallbackHandler(username, password), new LoginConfig(this.debug));
loginContext.login();
Subject jaasSubject = loginContext.getSubject();
if (LOG.isDebugEnabled()) {
LOG.debug("Kerberos authenticated user: " + jaasSubject);
}
String validatedUsername = jaasSubject.getPrincipals().iterator().next().toString();
Subject subjectCopy = JaasUtil.copySubject(jaasSubject);
result = new JaasSubjectHolder(subjectCopy, validatedUsername);
if (!this.multiTier) {
loginContext.logout();
}
}
catch (LoginException ex) {
throw new BadCredentialsException("Kerberos authentication failed", ex);
}
return result;
}
public void setDebug(boolean debug) {
this.debug = debug;
}
public void setMultiTier(boolean multiTier) {
this.multiTier = multiTier;
}
private static final class LoginConfig extends Configuration {
private boolean debug;
private LoginConfig(boolean debug) {
super();
this.debug = debug;
}
@Override
public AppConfigurationEntry[] getAppConfigurationEntry(String name) {
HashMap<String, String> options = new HashMap<String, String>();
options.put("storeKey", "true");
if (this.debug) {
options.put("debug", "true");
}
return new AppConfigurationEntry[] {
new AppConfigurationEntry("com.sun.security.auth.module.Krb5LoginModule",
AppConfigurationEntry.LoginModuleControlFlag.REQUIRED, options), };
}
}
static final class KerberosClientCallbackHandler implements CallbackHandler {
private String username;
private String password;
private KerberosClientCallbackHandler(String username, String password) {
this.username = username;
this.password = password;
}
@Override
public void handle(Callback[] callbacks) throws IOException, UnsupportedCallbackException {
for (Callback callback : callbacks) {
if (callback instanceof NameCallback) {
NameCallback ncb = (NameCallback) callback;
ncb.setName(this.username);
}
else if (callback instanceof PasswordCallback) {
PasswordCallback pwcb = (PasswordCallback) callback;
pwcb.setPassword(this.password.toCharArray());
}
else {
throw new UnsupportedCallbackException(callback,
"We got a " + callback.getClass().getCanonicalName()
+ ", but only NameCallback and PasswordCallback is supported");
}
}
}
}
}
@@ -0,0 +1,332 @@
/*
* Copyright 2004-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.kerberos.authentication.sun;
import java.security.Principal;
import java.security.PrivilegedActionException;
import java.security.PrivilegedExceptionAction;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Set;
import javax.security.auth.Subject;
import javax.security.auth.kerberos.KerberosPrincipal;
import javax.security.auth.login.AppConfigurationEntry;
import javax.security.auth.login.Configuration;
import javax.security.auth.login.LoginContext;
import com.sun.security.jgss.GSSUtil;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.ietf.jgss.GSSContext;
import org.ietf.jgss.GSSCredential;
import org.ietf.jgss.GSSManager;
import org.ietf.jgss.GSSName;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.kerberos.authentication.JaasSubjectHolder;
import org.springframework.security.kerberos.authentication.KerberosTicketValidation;
import org.springframework.security.kerberos.authentication.KerberosTicketValidator;
import org.springframework.util.Assert;
/**
* Implementation of {@link KerberosTicketValidator} which uses the SUN JAAS login module,
* which is included in the SUN JRE, it will not work with an IBM JRE. The whole
* configuration is done in this class, no additional JAAS configuration is needed.
*
* @author Mike Wiesner
* @author Jeremy Stone
* @author Bogdan Mustiata
* @since 1.0
*/
public class SunJaasKerberosTicketValidator implements KerberosTicketValidator, InitializingBean {
private String servicePrincipal;
private String realmName;
private Resource keyTabLocation;
private Subject serviceSubject;
private boolean holdOnToGSSContext;
private boolean debug = false;
private boolean multiTier = false;
private boolean refreshKrb5Config = false;
private static final Log LOG = LogFactory.getLog(SunJaasKerberosTicketValidator.class);
@Override
public KerberosTicketValidation validateTicket(byte[] token) {
try {
if (!this.multiTier) {
return Subject.doAs(this.serviceSubject, new KerberosValidateAction(token));
}
Subject subjectCopy = JaasUtil.copySubject(this.serviceSubject);
JaasSubjectHolder subjectHolder = new JaasSubjectHolder(subjectCopy);
return Subject.doAs(subjectHolder.getJaasSubject(), new KerberosMultitierValidateAction(token));
}
catch (PrivilegedActionException ex) {
throw new BadCredentialsException("Kerberos validation not successful", ex);
}
}
@Override
public void afterPropertiesSet() throws Exception {
Assert.notNull(this.servicePrincipal, "servicePrincipal must be specified");
Assert.notNull(this.keyTabLocation, "keyTab must be specified");
if (this.keyTabLocation instanceof ClassPathResource) {
this.LOG.warn(
"Your keytab is in the classpath. This file needs special protection and shouldn't be in the classpath. JAAS may also not be able to load this file from classpath.");
}
String keyTabLocationAsString = this.keyTabLocation.getURL().toExternalForm();
// We need to remove the file prefix (if there is one), as it is not supported in
// Java 7 anymore.
// As Java 6 accepts it with and without the prefix, we don't need to check for
// Java 7
if (keyTabLocationAsString.startsWith("file:")) {
keyTabLocationAsString = keyTabLocationAsString.substring(5);
}
LoginConfig loginConfig = new LoginConfig(keyTabLocationAsString, this.servicePrincipal, this.realmName,
this.multiTier, this.debug, this.refreshKrb5Config);
Set<Principal> princ = new HashSet<Principal>(1);
princ.add(new KerberosPrincipal(this.servicePrincipal));
Subject sub = new Subject(false, princ, new HashSet<Object>(), new HashSet<Object>());
LoginContext lc = new LoginContext("", sub, null, loginConfig);
lc.login();
this.serviceSubject = lc.getSubject();
}
/**
* The service principal of the application. For web apps this is
* <code>HTTP/full-qualified-domain-name@DOMAIN</code>. The keytab must contain the
* key for this principal.
* @param servicePrincipal service principal to use
* @see #setKeyTabLocation(Resource)
*/
public void setServicePrincipal(String servicePrincipal) {
this.servicePrincipal = servicePrincipal;
}
/**
* The realm name of the application. For web apps this is <code>DOMAIN</code>
* @param realmName
*/
public void setRealmName(String realmName) {
this.realmName = realmName;
}
/**
* @param multiTier
*/
public void setMultiTier(boolean multiTier) {
this.multiTier = multiTier;
}
/**
* <p>
* The location of the keytab. You can use the normale Spring Resource prefixes like
* <code>file:</code> or <code>classpath:</code>, but as the file is later on read by
* JAAS, we cannot guarantee that <code>classpath</code> works in every environment,
* esp. not in Java EE application servers. You should use <code>file:</code> there.
*
* This file also needs special protection, which is another reason to not include it
* in the classpath but rather use <code>file:/etc/http.keytab</code> for example.
* @param keyTabLocation The location where the keytab resides
*/
public void setKeyTabLocation(Resource keyTabLocation) {
this.keyTabLocation = keyTabLocation;
}
/**
* Enables the debug mode of the JAAS Kerberos login module.
* @param debug default is false
*/
public void setDebug(boolean debug) {
this.debug = debug;
}
/**
* Determines whether to hold on to the {@link GSSContext GSS security context} or
* otherwise {@link GSSContext#dispose() dispose} of it immediately (the default
* behaviour).
* <p>
* Holding on to the GSS context allows decrypt and encrypt operations for subsequent
* interactions with the principal.
* @param holdOnToGSSContext true if should hold on to context
*/
public void setHoldOnToGSSContext(boolean holdOnToGSSContext) {
this.holdOnToGSSContext = holdOnToGSSContext;
}
/**
* Enables configuration to be refreshed before the login method is called.
* @param refreshKrb5Config Set this to true, if you want the configuration to be
* refreshed before the login method is called.
*/
public void setRefreshKrb5Config(boolean refreshKrb5Config) {
this.refreshKrb5Config = refreshKrb5Config;
}
/**
* This class is needed, because the validation must run with previously generated
* JAAS subject which belongs to the service principal and was loaded out of the
* keytab during startup.
*/
private final class KerberosMultitierValidateAction implements PrivilegedExceptionAction<KerberosTicketValidation> {
byte[] kerberosTicket;
private KerberosMultitierValidateAction(byte[] kerberosTicket) {
this.kerberosTicket = kerberosTicket;
}
@Override
public KerberosTicketValidation run() throws Exception {
byte[] responseToken = new byte[0];
GSSManager manager = GSSManager.getInstance();
GSSContext context = manager.createContext((GSSCredential) null);
while (!context.isEstablished()) {
context.acceptSecContext(this.kerberosTicket, 0, this.kerberosTicket.length);
}
Subject subject = GSSUtil.createSubject(context.getSrcName(), context.getDelegCred());
KerberosTicketValidation result = new KerberosTicketValidation(context.getSrcName().toString(), subject,
responseToken, context);
if (!SunJaasKerberosTicketValidator.this.holdOnToGSSContext) {
context.dispose();
}
return result;
}
}
/**
* This class is needed, because the validation must run with previously generated
* JAAS subject which belongs to the service principal and was loaded out of the
* keytab during startup.
*/
private final class KerberosValidateAction implements PrivilegedExceptionAction<KerberosTicketValidation> {
byte[] kerberosTicket;
private KerberosValidateAction(byte[] kerberosTicket) {
this.kerberosTicket = kerberosTicket;
}
@Override
public KerberosTicketValidation run() throws Exception {
byte[] responseToken = new byte[0];
GSSName gssName = null;
GSSContext context = GSSManager.getInstance().createContext((GSSCredential) null);
while (!context.isEstablished()) {
responseToken = context.acceptSecContext(this.kerberosTicket, 0, this.kerberosTicket.length);
gssName = context.getSrcName();
if (gssName == null) {
throw new BadCredentialsException("GSSContext name of the context initiator is null");
}
}
GSSCredential delegationCredential = null;
if (context.getCredDelegState()) {
delegationCredential = context.getDelegCred();
}
if (!SunJaasKerberosTicketValidator.this.holdOnToGSSContext) {
context.dispose();
}
return new KerberosTicketValidation(gssName.toString(),
SunJaasKerberosTicketValidator.this.servicePrincipal, responseToken, context, delegationCredential);
}
}
/**
* Normally you need a JAAS config file in order to use the JAAS Kerberos Login
* Module, with this class it is not needed and you can have different configurations
* in one JVM.
*/
private static final class LoginConfig extends Configuration {
private String keyTabLocation;
private String servicePrincipalName;
private String realmName;
private boolean multiTier;
private boolean debug;
private boolean refreshKrb5Config;
private LoginConfig(String keyTabLocation, String servicePrincipalName, String realmName, boolean multiTier,
boolean debug, boolean refreshKrb5Config) {
this.keyTabLocation = keyTabLocation;
this.servicePrincipalName = servicePrincipalName;
this.realmName = realmName;
this.multiTier = multiTier;
this.debug = debug;
this.refreshKrb5Config = refreshKrb5Config;
}
@Override
public AppConfigurationEntry[] getAppConfigurationEntry(String name) {
HashMap<String, String> options = new HashMap<String, String>();
options.put("useKeyTab", "true");
options.put("keyTab", this.keyTabLocation);
options.put("principal", this.servicePrincipalName);
options.put("storeKey", "true");
options.put("doNotPrompt", "true");
if (this.debug) {
options.put("debug", "true");
}
if (this.realmName != null) {
options.put("realm", this.realmName);
}
if (this.refreshKrb5Config) {
options.put("refreshKrb5Config", "true");
}
if (!this.multiTier) {
options.put("isInitiator", "false");
}
return new AppConfigurationEntry[] {
new AppConfigurationEntry("com.sun.security.auth.module.Krb5LoginModule",
AppConfigurationEntry.LoginModuleControlFlag.REQUIRED, options), };
}
}
}
@@ -0,0 +1,92 @@
/*
* Copyright 2004-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.kerberos.authentication;
import java.util.List;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
/**
* Test class for {@link KerberosAuthenticationProvider}
*
* @author Mike Wiesner
* @since 1.0
*/
public class KerberosAuthenticationProviderTests {
private KerberosAuthenticationProvider provider;
private KerberosClient kerberosClient;
private UserDetailsService userDetailsService;
private static final String TEST_USER = "Testuser@SPRINGSOURCE.ORG";
private static final String TEST_PASSWORD = "password";
private static final UsernamePasswordAuthenticationToken INPUT_TOKEN = new UsernamePasswordAuthenticationToken(
TEST_USER, TEST_PASSWORD);
private static final List<GrantedAuthority> AUTHORITY_LIST = AuthorityUtils.createAuthorityList("ROLE_ADMIN");
private static final UserDetails USER_DETAILS = new User(TEST_USER, "empty", true, true, true, true,
AUTHORITY_LIST);
private static final JaasSubjectHolder JAAS_SUBJECT_HOLDER = new JaasSubjectHolder(null, TEST_USER);
@BeforeEach
public void before() {
// mocking
this.kerberosClient = mock(KerberosClient.class);
this.userDetailsService = mock(UserDetailsService.class);
this.provider = new KerberosAuthenticationProvider();
this.provider.setKerberosClient(this.kerberosClient);
this.provider.setUserDetailsService(this.userDetailsService);
}
@Test
public void testLoginOk() throws Exception {
given(this.userDetailsService.loadUserByUsername(TEST_USER)).willReturn(USER_DETAILS);
given(this.kerberosClient.login(TEST_USER, TEST_PASSWORD)).willReturn(JAAS_SUBJECT_HOLDER);
Authentication authenticate = this.provider.authenticate(INPUT_TOKEN);
verify(this.kerberosClient).login(TEST_USER, TEST_PASSWORD);
assertThat(authenticate).isNotNull();
assertThat(authenticate.getName()).isEqualTo(TEST_USER);
assertThat(authenticate.getPrincipal()).isEqualTo(USER_DETAILS);
assertThat(authenticate.getCredentials()).isEqualTo(TEST_PASSWORD);
assertThat(authenticate.getAuthorities()).isEqualTo(AUTHORITY_LIST);
}
}
@@ -0,0 +1,173 @@
/*
* Copyright 2004-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.kerberos.authentication;
import java.util.List;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.security.authentication.AccountExpiredException;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.CredentialsExpiredException;
import org.springframework.security.authentication.DisabledException;
import org.springframework.security.authentication.LockedException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.mock;
/**
* Test class for {@link KerberosServiceAuthenticationProvider}
*
* @author Mike Wiesner
* @author Jeremy Stone
* @since 1.0
*/
public class KerberosServiceAuthenticationProviderTests {
private KerberosServiceAuthenticationProvider provider;
private KerberosTicketValidator ticketValidator;
private UserDetailsService userDetailsService;
// data
private static final byte[] TEST_TOKEN = "TestToken".getBytes();
private static final byte[] RESPONSE_TOKEN = "ResponseToken".getBytes();
private static final String TEST_USER = "Testuser@SPRINGSOURCE.ORG";
private static final KerberosTicketValidation TICKET_VALIDATION = new KerberosTicketValidation(TEST_USER,
"XXX@test.com", RESPONSE_TOKEN, null);
private static final List<GrantedAuthority> AUTHORITY_LIST = AuthorityUtils.createAuthorityList("ROLE_ADMIN");
private static final UserDetails USER_DETAILS = new User(TEST_USER, "empty", true, true, true, true,
AUTHORITY_LIST);
private static final KerberosServiceRequestToken INPUT_TOKEN = new KerberosServiceRequestToken(TEST_TOKEN);
@BeforeEach
public void before() {
System.setProperty("java.security.krb5.conf", "test.com");
System.setProperty("java.security.krb5.kdc", "kdc.test.com");
// mocking
this.ticketValidator = mock(KerberosTicketValidator.class);
this.userDetailsService = mock(UserDetailsService.class);
this.provider = new KerberosServiceAuthenticationProvider();
this.provider.setTicketValidator(this.ticketValidator);
this.provider.setUserDetailsService(this.userDetailsService);
}
@AfterEach
public void after() {
System.clearProperty("java.security.krb5.conf");
System.clearProperty("java.security.krb5.kdc");
}
@Test
public void testEverythingWorks() throws Exception {
Authentication output = callProviderAndReturnUser(USER_DETAILS, INPUT_TOKEN);
assertThat(output).isNotNull();
assertThat(output.getName()).isEqualTo(TEST_USER);
assertThat(output.getAuthorities()).isEqualTo(AUTHORITY_LIST);
assertThat(output.getPrincipal()).isEqualTo(USER_DETAILS);
}
@Test
public void testAuthenticationDetailsPropagation() throws Exception {
KerberosServiceRequestToken requestToken = new KerberosServiceRequestToken(TEST_TOKEN);
requestToken.setDetails("TestDetails");
Authentication output = callProviderAndReturnUser(USER_DETAILS, requestToken);
assertThat(output).isNotNull();
assertThat(output.getDetails()).isEqualTo(requestToken.getDetails());
}
@Test
public void testUserIsDisabled() throws Exception {
assertThatExceptionOfType(DisabledException.class).isThrownBy(() -> {
User disabledUser = new User(TEST_USER, "empty", false, true, true, true, AUTHORITY_LIST);
callProviderAndReturnUser(disabledUser, INPUT_TOKEN);
});
}
@Test
public void testUserAccountIsExpired() throws Exception {
assertThatExceptionOfType(AccountExpiredException.class).isThrownBy(() -> {
User expiredUser = new User(TEST_USER, "empty", true, false, true, true, AUTHORITY_LIST);
callProviderAndReturnUser(expiredUser, INPUT_TOKEN);
}).isInstanceOf(AccountExpiredException.class);
}
@Test
public void testUserCredentialsExpired() throws Exception {
assertThatExceptionOfType(CredentialsExpiredException.class).isThrownBy(() -> {
User credExpiredUser = new User(TEST_USER, "empty", true, true, false, true, AUTHORITY_LIST);
callProviderAndReturnUser(credExpiredUser, INPUT_TOKEN);
});
}
@Test
public void testUserAccountLockedCredentialsExpired() throws Exception {
assertThatExceptionOfType(LockedException.class).isThrownBy(() -> {
User lockedUser = new User(TEST_USER, "empty", true, true, true, false, AUTHORITY_LIST);
callProviderAndReturnUser(lockedUser, INPUT_TOKEN);
});
}
@Test
public void testUsernameNotFound() throws Exception {
// stubbing
given(this.ticketValidator.validateTicket(TEST_TOKEN)).willReturn(TICKET_VALIDATION);
given(this.userDetailsService.loadUserByUsername(TEST_USER)).willThrow(new UsernameNotFoundException(""));
// testing
assertThatExceptionOfType(UsernameNotFoundException.class)
.isThrownBy(() -> this.provider.authenticate(INPUT_TOKEN));
}
@Test
public void testTicketValidationWrong() throws Exception {
// stubbing
given(this.ticketValidator.validateTicket(TEST_TOKEN)).willThrow(new BadCredentialsException(""));
// testing
assertThatExceptionOfType(BadCredentialsException.class)
.isThrownBy(() -> this.provider.authenticate(INPUT_TOKEN));
}
private Authentication callProviderAndReturnUser(UserDetails userDetails, Authentication inputToken) {
// stubbing
given(this.ticketValidator.validateTicket(TEST_TOKEN)).willReturn(TICKET_VALIDATION);
given(this.userDetailsService.loadUserByUsername(TEST_USER)).willReturn(userDetails);
// testing
return this.provider.authenticate(inputToken);
}
}
@@ -0,0 +1,68 @@
/*
* Copyright 2004-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.kerberos.authentication;
import javax.security.auth.Subject;
import org.ietf.jgss.GSSContext;
import org.ietf.jgss.GSSCredential;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;
public class KerberosTicketValidationTests {
private String username = "username";
private Subject subject = new Subject();
private byte[] responseToken = "token".getBytes();
private GSSContext gssContext = mock(GSSContext.class);
private GSSCredential delegationCredential = mock(GSSCredential.class);
@Test
public void createResultOfTicketValidationWithSubject() {
KerberosTicketValidation ticketValidation = new KerberosTicketValidation(this.username, this.subject,
this.responseToken, this.gssContext);
assertThat(ticketValidation.username()).isEqualTo(this.username);
assertThat(ticketValidation.responseToken()).isEqualTo(this.responseToken);
assertThat(ticketValidation.getGssContext()).isEqualTo(this.gssContext);
assertThat(ticketValidation.getDelegationCredential()).withFailMessage("With no credential delegation")
.isNull();
}
@Test
public void createResultOfTicketValidationWithSubjectAndDelegation() {
KerberosTicketValidation ticketValidation = new KerberosTicketValidation(this.username, this.subject,
this.responseToken, this.gssContext, this.delegationCredential);
assertThat(ticketValidation.username()).isEqualTo(this.username);
assertThat(ticketValidation.responseToken()).isEqualTo(this.responseToken);
assertThat(ticketValidation.getGssContext()).isEqualTo(this.gssContext);
assertThat(ticketValidation.getDelegationCredential()).withFailMessage("With credential delegation")
.isEqualTo(this.delegationCredential);
}
}
@@ -0,0 +1,91 @@
/*
* Copyright 2004-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.kerberos.authentication.sun;
import java.util.Base64;
import org.junit.jupiter.api.Test;
import org.springframework.security.authentication.BadCredentialsException;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
public class SunJaasKerberosTicketValidatorTests {
// copy of token taken from a test where windows host
// is trying to authenticate with spnego. nothing sensitive here
private static String header = "YIIGXAYGKwYBBQUCoIIGUDCCBkygMDAuBgkqhkiC9xIBAgIGCSqGSIb3EgEC"
+ "AgYKKwYBBAGCNwICHgYKKwYBBAGCNwICCqKCBhYEggYSYIIGDgYJKoZIhvcS"
+ "AQICAQBuggX9MIIF+aADAgEFoQMCAQ6iBwMFACAAAACjggSFYYIEgTCCBH2g"
+ "AwIBBaENGwtFWEFNUExFLk9SR6IiMCCgAwIBAqEZMBcbBEhUVFAbD25lby5l"
+ "eGFtcGxlLm9yZ6OCBEEwggQ9oAMCARehAwIBA6KCBC8EggQrD8vaEz0V5W5n"
+ "PZINBBxp1yCVZOn4kpHzfNtqj9F3L/6MzrTo9bP2l0UhxCQIKo+ixUMJgQAs"
+ "Xd82tF4JEsSt90pyv8f751pH3UeqCOhssTcXhJpTKQmYlAro+t3klpT6/c/r"
+ "4KX+wqM++19IjWE2CJpyloo/5Wi9Kwk83bjO6UfCTreqkd+eIPM16rf8p/wH"
+ "KYj+ssla4y+IvwvZvAW8TXuth8opiqeLvt5H0GWkwuJhrZu6cHlSWZAMtRQg"
+ "TSZCS/0LCiZVCyNNCpvvXbyp8p5T6ImKPfMO5l8VJKgdrmCOlAQYFwTpG0MD"
+ "1e9LUvk/Fh7OoeglJAygTRgbvIGDAuexw7o6MHbj+XhXvEtC6kUEwHuG5C/1"
+ "5Q327FRLfMeL8YcdU6YZ06wNmUmDPGqy+WHlEaFM7G38u/oKKS4cKIZKi8PL"
+ "hpVPvjU+uIOJVuIP882IxCW7rcqaRCleYCp7YAQbjussrCS0DSRKPEy60bv0"
+ "MIkh71lCY5/KwQloEDMqav12+1wtWTnmLAkfglGjgb1Q7fb79h58nnTBJAwI"
+ "e6Bv72XYdgcU1orDQVlylAk9trxDP42yOGuG5IozJTIn+9zPOvM5CGgTCzZv"
+ "4wInGa1Stuz11WwaIenwGbpCXWSP4uoe9TLpKVzJUmLd8dpZ0YjpuFNBGnHz"
+ "1LG0Q9aUni7nl7seKVc2AnuBqS+mlS+/In0LaEW4k0GctgMqfVyP2mmb7ur+"
+ "wl4YjAVRFhPMSSy4AYftRYoIUGad97VcZx107pD0v/gE1Eu4iqTomqJBOaWJ"
+ "gqnjmf6A8P9IHbeVx/zbnKYp8nC+M57jpFcy9GKVh3DIXkbSBHQ+feamGBJn"
+ "AxTpeix/DN5u91azJaB9RlfIvQYGLGaxupCXpjVfhTSJHvoA6sOUObgK3/hQ"
+ "7Gj81FR+C8AfrHzOPPD2S14pkL7n2WC6jOTHrghxm7/iXcreDHos/1OuPFk0"
+ "9wbrCWgF9tHAuXQJW/zxjYg9CUboJ51+ZposfmABTKoUKeFY4zgVyuEwE2YO"
+ "hn7OLsfbXalmF5IPAlNibAIIFVos1u+14oFOYivIXEEgpvZMhvFOuGaqrHHR"
+ "xRBQ/z8nogMVGyCukFH/tg5N8IX9X+VQ1U43rf4IYaCJ0no5skmStf7fmcUJ"
+ "+3KXhKfP4TKrSIDdo313GW/6rIM2wo4RPdjQ1LlX+EAb8X73W0OZLumtvhm9"
+ "1jL2pWFL/mTGEGkPd7Od29h7JYcvwdDCjkIzIlrbzFJyyTU3ATaMyrvDZKys"
+ "ZSJ2m3v7Y0E/Cw+/T8SG3HeSjJ2e/dsjJRpv+6RxXzdNWKKCUN3UFEH0QfAk"
+ "6s8avEF767U87Df7BBCuecxIJAUL+kBBsYuDCw8FP0AOxOIjh9EX/EopeJpi"
+ "e1ekNGvUK+mhj3WgjCExEe60y4FoENKkggFZMIIBVaADAgEXooIBTASCAUgR"
+ "/FTo9JsQB4yInDswmvHiOyJYGdA9jv72rjvJfdHejaU6L8QHj0DPMdGWxAXI"
+ "aqLrANjOOSGb9HEdt9QUd/zvi8fBEEZgWIX0nUUrvN9wsKEB1jxmlAx87mf7"
+ "2Kyo9z7mdlFBG49mq/jjFFLtiVJxHfea4B4VGRUodNRLWUY7H05ruJZQbeUF"
+ "UgYMsiMC59oi82OR3re8gpypecrtD0g88CwCrReDpoLb7VGVCc4z00ld7ugz"
+ "EbGsZvh0SLMKnxAAm1nYlqQTu/VKC8zi9N0c7ikJegGwBKOgbebPm+ckKDra"
+ "fbVsm0pcmnXv5WvwjJPFjJWsL+7NzUfsedJxgHTCzdztZyNxu6iQf8cpAabp"
+ "PB1vJdIMjc8benP9/+EUhX1LkwvV/rOO3ocwjtdLY1rcmNXSbhnf8jDcVjOe" + "eL2PHBfvkne/FgxC";
// @Rule
// public ExpectedException thrown = ExpectedException.none();
// @Test
// public void testJdkMsKrb5OIDRegressionTweak() throws Exception {
// thrown.expect(BadCredentialsException.class);
// thrown.expectMessage(not(containsString("GSSContext name of the context initiator
// is null")));
// thrown.expectMessage(containsString("Kerberos validation not successful"));
// SunJaasKerberosTicketValidator validator = new SunJaasKerberosTicketValidator();
// byte[] kerberosTicket = Base64.decode(header.getBytes());
// validator.validateTicket(kerberosTicket);
// }
@Test
public void testJdkMsKrb5OIDRegressionTweak() {
assertThatExceptionOfType(BadCredentialsException.class).isThrownBy(() -> {
SunJaasKerberosTicketValidator validator = new SunJaasKerberosTicketValidator();
byte[] kerberosTicket = Base64.getDecoder().decode(header.getBytes());
validator.validateTicket(kerberosTicket);
}).withMessage("Kerberos validation not successful");
}
}
@@ -0,0 +1,16 @@
plugins {
id 'io.spring.convention.spring-module'
}
description = 'Spring Security Kerberos Test'
dependencies {
management platform(project(":spring-security-dependencies"))
api libs.org.apache.kerby.simplekdc
api 'org.junit.jupiter:junit-jupiter'
testImplementation 'org.springframework:spring-test'
testImplementation 'org.mockito:mockito-junit-jupiter'
testImplementation libs.org.assertj.assertj.core
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
}
@@ -0,0 +1,88 @@
/*
* Copyright 2004-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.kerberos.test;
import java.io.File;
import java.util.Properties;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
/**
* KerberosSecurityTestcase provides a base class for using MiniKdc with other testcases.
* KerberosSecurityTestcase starts the MiniKdc (@Before) before running tests, and stop
* the MiniKdc (@After) after the testcases, using default settings (working dir and kdc
* configurations).
* <p>
* Users can directly inherit this class and implement their own test functions using the
* default settings, or override functions getTestDir() and createMiniKdcConf() to provide
* new settings.
*
*/
public class KerberosSecurityTestcase {
private MiniKdc kdc;
private File workDir;
private Properties conf;
@BeforeEach
public void startMiniKdc() throws Exception {
createTestDir();
createMiniKdcConf();
this.kdc = new MiniKdc(this.conf, this.workDir);
this.kdc.start();
}
/**
* Create a working directory, it should be the build directory. Under this directory
* an ApacheDS working directory will be created, this directory will be deleted when
* the MiniKdc stops.
*/
public void createTestDir() {
this.workDir = new File(System.getProperty("test.dir", "target"));
}
/**
* Create a Kdc configuration
*/
public void createMiniKdcConf() {
this.conf = MiniKdc.createConf();
}
@AfterEach
public void stopMiniKdc() {
if (this.kdc != null) {
this.kdc.stop();
}
}
public MiniKdc getKdc() {
return this.kdc;
}
public File getWorkDir() {
return this.workDir;
}
public Properties getConf() {
return this.conf;
}
}
@@ -0,0 +1,429 @@
/*
* Copyright 2004-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.kerberos.test;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Locale;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import org.apache.kerby.kerberos.kerb.KrbException;
import org.apache.kerby.kerberos.kerb.server.KdcConfigKey;
import org.apache.kerby.kerberos.kerb.server.SimpleKdcServer;
import org.apache.kerby.util.IOUtil;
import org.apache.kerby.util.NetworkUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Mini KDC based on Apache Directory Server that can be embedded in testcases or used
* from command line as a standalone KDC.
* <p>
* <b>From within testcases:</b>
* <p>
* MiniKdc sets one System property when started and un-set when stopped:
* <ul>
* <li>sun.security.krb5.debug: set to the debug value provided in the configuration</li>
* </ul>
* Because of this, multiple MiniKdc instances cannot be started in parallel. For example,
* running testcases in parallel that start a KDC each. To accomplish this a single
* MiniKdc should be used for all testcases running in parallel.
* <p>
* MiniKdc default configuration values are:
* <ul>
* <li>org.name=EXAMPLE (used to create the REALM)</li>
* <li>org.domain=COM (used to create the REALM)</li>
* <li>kdc.bind.address=localhost</li>
* <li>kdc.port=0 (ephemeral port)</li>
* <li>instance=DefaultKrbServer</li>
* <li>max.ticket.lifetime=86400000 (1 day)</li>
* <li>max.renewable.lifetime=604800000 (7 days)</li>
* <li>transport=TCP</li>
* <li>debug=false</li>
* </ul>
* The generated krb5.conf forces TCP connections.
*
* @author Original Hadoop MiniKdc Authors
* @author Janne Valkealahti
* @author Bogdan Mustiata
*/
public class MiniKdc {
public static final String JAVA_SECURITY_KRB5_CONF = "java.security.krb5.conf";
public static final String SUN_SECURITY_KRB5_DEBUG = "sun.security.krb5.debug";
public static void main(String[] args) throws Exception {
if (args.length < 4) {
System.out.println("Arguments: <WORKDIR> <MINIKDCPROPERTIES> " + "<KEYTABFILE> [<PRINCIPALS>]+");
System.exit(1);
}
File workDir = new File(args[0]);
if (!workDir.exists()) {
throw new RuntimeException("Specified work directory does not exists: " + workDir.getAbsolutePath());
}
Properties conf = createConf();
File file = new File(args[1]);
if (!file.exists()) {
throw new RuntimeException("Specified configuration does not exists: " + file.getAbsolutePath());
}
Properties userConf = new Properties();
InputStreamReader r = null;
try {
r = new InputStreamReader(new FileInputStream(file), StandardCharsets.UTF_8);
userConf.load(r);
}
finally {
if (r != null) {
r.close();
}
}
for (Map.Entry<?, ?> entry : userConf.entrySet()) {
conf.put(entry.getKey(), entry.getValue());
}
final MiniKdc miniKdc = new MiniKdc(conf, workDir);
miniKdc.start();
File krb5conf = new File(workDir, "krb5.conf");
if (miniKdc.getKrb5conf().renameTo(krb5conf)) {
File keytabFile = new File(args[2]).getAbsoluteFile();
String[] principals = new String[args.length - 3];
System.arraycopy(args, 3, principals, 0, args.length - 3);
miniKdc.createPrincipal(keytabFile, principals);
System.out.println();
System.out.println("Standalone MiniKdc Running");
System.out.println("---------------------------------------------------");
System.out.println(" Realm : " + miniKdc.getRealm());
System.out.println(" Running at : " + miniKdc.getHost() + ":" + miniKdc.getPort());
System.out.println(" krb5conf : " + krb5conf);
System.out.println();
System.out.println(" created keytab : " + keytabFile);
System.out.println(" with principals : " + Arrays.asList(principals));
System.out.println();
System.out.println(" Do <CTRL-C> or kill <PID> to stop it");
System.out.println("---------------------------------------------------");
System.out.println();
Runtime.getRuntime().addShutdownHook(new Thread() {
@Override
public void run() {
miniKdc.stop();
}
});
}
else {
throw new RuntimeException("Cannot rename KDC's krb5conf to " + krb5conf.getAbsolutePath());
}
}
private static final Logger LOG = LoggerFactory.getLogger(MiniKdc.class);
public static final String ORG_NAME = "org.name";
public static final String ORG_DOMAIN = "org.domain";
public static final String KDC_BIND_ADDRESS = "kdc.bind.address";
public static final String KDC_PORT = "kdc.port";
public static final String INSTANCE = "instance";
public static final String MAX_TICKET_LIFETIME = "max.ticket.lifetime";
public static final String MIN_TICKET_LIFETIME = "min.ticket.lifetime";
public static final String MAX_RENEWABLE_LIFETIME = "max.renewable.lifetime";
public static final String TRANSPORT = "transport";
public static final String DEBUG = "debug";
private static final Set<String> PROPERTIES = new HashSet<String>();
private static final Properties DEFAULT_CONFIG = new Properties();
static {
PROPERTIES.add(ORG_NAME);
PROPERTIES.add(ORG_DOMAIN);
PROPERTIES.add(KDC_BIND_ADDRESS);
PROPERTIES.add(KDC_BIND_ADDRESS);
PROPERTIES.add(KDC_PORT);
PROPERTIES.add(INSTANCE);
PROPERTIES.add(TRANSPORT);
PROPERTIES.add(MAX_TICKET_LIFETIME);
PROPERTIES.add(MAX_RENEWABLE_LIFETIME);
DEFAULT_CONFIG.setProperty(KDC_BIND_ADDRESS, "localhost");
DEFAULT_CONFIG.setProperty(KDC_PORT, "0");
DEFAULT_CONFIG.setProperty(INSTANCE, "DefaultKrbServer");
DEFAULT_CONFIG.setProperty(ORG_NAME, "EXAMPLE");
DEFAULT_CONFIG.setProperty(ORG_DOMAIN, "COM");
DEFAULT_CONFIG.setProperty(TRANSPORT, "TCP");
DEFAULT_CONFIG.setProperty(MAX_TICKET_LIFETIME, "86400000");
DEFAULT_CONFIG.setProperty(MAX_RENEWABLE_LIFETIME, "604800000");
DEFAULT_CONFIG.setProperty(DEBUG, "false");
}
/**
* Convenience method that returns MiniKdc default configuration.
* <p>
* The returned configuration is a copy, it can be customized before using it to
* create a MiniKdc.
* @return a MiniKdc default configuration.
*/
public static Properties createConf() {
return (Properties) DEFAULT_CONFIG.clone();
}
private Properties conf;
private SimpleKdcServer simpleKdc;
private int port;
private String realm;
private File workDir;
private File krb5conf;
private String transport;
private boolean krb5Debug;
public void setTransport(String transport) {
this.transport = transport;
}
/**
* Creates a MiniKdc.
* @param conf MiniKdc configuration.
* @param workDir working directory, it should be the build directory. Under this
* directory an ApacheDS working directory will be created, this directory will be
* deleted when the MiniKdc stops.
* @throws Exception thrown if the MiniKdc could not be created.
*/
public MiniKdc(Properties conf, File workDir) throws Exception {
if (!conf.keySet().containsAll(PROPERTIES)) {
Set<String> missingProperties = new HashSet<String>(PROPERTIES);
missingProperties.removeAll(conf.keySet());
throw new IllegalArgumentException("Missing configuration properties: " + missingProperties);
}
this.workDir = new File(workDir, Long.toString(System.currentTimeMillis()));
if (!this.workDir.exists() && !this.workDir.mkdirs()) {
throw new RuntimeException("Cannot create directory " + this.workDir);
}
LOG.info("Configuration:");
LOG.info("---------------------------------------------------------------");
for (Map.Entry<?, ?> entry : conf.entrySet()) {
LOG.info(" {}: {}", entry.getKey(), entry.getValue());
}
LOG.info("---------------------------------------------------------------");
this.conf = conf;
this.port = Integer.parseInt(conf.getProperty(KDC_PORT));
String orgName = conf.getProperty(ORG_NAME);
String orgDomain = conf.getProperty(ORG_DOMAIN);
this.realm = orgName.toUpperCase(Locale.ENGLISH) + "." + orgDomain.toUpperCase(Locale.ENGLISH);
}
/**
* Returns the port of the MiniKdc.
* @return the port of the MiniKdc.
*/
public int getPort() {
return this.port;
}
/**
* Returns the host of the MiniKdc.
* @return the host of the MiniKdc.
*/
public String getHost() {
return this.conf.getProperty(KDC_BIND_ADDRESS);
}
/**
* Returns the realm of the MiniKdc.
* @return the realm of the MiniKdc.
*/
public String getRealm() {
return this.realm;
}
public File getKrb5conf() {
this.krb5conf = new File(System.getProperty(JAVA_SECURITY_KRB5_CONF));
return this.krb5conf;
}
/**
* Starts the MiniKdc.
* @throws Exception thrown if the MiniKdc could not be started.
*/
public synchronized void start() throws Exception {
if (this.simpleKdc != null) {
throw new RuntimeException("Already started");
}
this.simpleKdc = new SimpleKdcServer();
prepareKdcServer();
this.simpleKdc.init();
resetDefaultRealm();
this.simpleKdc.start();
LOG.info("MiniKdc started.");
}
private void resetDefaultRealm() throws IOException {
InputStream templateResource = new FileInputStream(getKrb5conf().getAbsolutePath());
String content = IOUtil.readInput(templateResource);
content = content.replaceAll("default_realm = .*\n", "default_realm = " + getRealm() + "\n");
IOUtil.writeFile(content, getKrb5conf());
}
private void prepareKdcServer() throws Exception {
// transport
this.simpleKdc.setWorkDir(this.workDir);
this.simpleKdc.setKdcHost(getHost());
this.simpleKdc.setKdcRealm(this.realm);
if (this.transport == null) {
this.transport = this.conf.getProperty(TRANSPORT);
}
if (this.port == 0) {
this.port = NetworkUtil.getServerPort();
}
if (this.transport != null) {
if (this.transport.trim().equals("TCP")) {
this.simpleKdc.setKdcTcpPort(this.port);
this.simpleKdc.setAllowUdp(false);
}
else if (this.transport.trim().equals("UDP")) {
this.simpleKdc.setKdcUdpPort(this.port);
this.simpleKdc.setAllowTcp(false);
}
else {
throw new IllegalArgumentException("Invalid transport: " + this.transport);
}
}
else {
throw new IllegalArgumentException("Need to set transport!");
}
this.simpleKdc.getKdcConfig().setString(KdcConfigKey.KDC_SERVICE_NAME, this.conf.getProperty(INSTANCE));
if (this.conf.getProperty(DEBUG) != null) {
this.krb5Debug = getAndSet(SUN_SECURITY_KRB5_DEBUG, this.conf.getProperty(DEBUG));
}
if (this.conf.getProperty(MIN_TICKET_LIFETIME) != null) {
this.simpleKdc.getKdcConfig()
.setLong(KdcConfigKey.MINIMUM_TICKET_LIFETIME,
Long.parseLong(this.conf.getProperty(MIN_TICKET_LIFETIME)));
}
if (this.conf.getProperty(MAX_TICKET_LIFETIME) != null) {
this.simpleKdc.getKdcConfig()
.setLong(KdcConfigKey.MAXIMUM_TICKET_LIFETIME,
Long.parseLong(this.conf.getProperty(MiniKdc.MAX_TICKET_LIFETIME)));
}
}
/**
* Stops the MiniKdc
*/
public synchronized void stop() {
if (this.simpleKdc != null) {
try {
this.simpleKdc.stop();
}
catch (KrbException ex) {
ex.printStackTrace();
}
finally {
if (this.conf.getProperty(DEBUG) != null) {
System.setProperty(SUN_SECURITY_KRB5_DEBUG, Boolean.toString(this.krb5Debug));
}
}
}
delete(this.workDir);
try {
// Will be fixed in next Kerby version.
Thread.sleep(1000);
}
catch (InterruptedException ex) {
ex.printStackTrace();
}
LOG.info("MiniKdc stopped.");
}
private void delete(File f) {
if (f.isFile()) {
if (!f.delete()) {
LOG.warn("WARNING: cannot delete file " + f.getAbsolutePath());
}
}
else {
File[] fileList = f.listFiles();
if (fileList != null) {
for (File c : fileList) {
delete(c);
}
}
if (!f.delete()) {
LOG.warn("WARNING: cannot delete directory " + f.getAbsolutePath());
}
}
}
/**
* Creates a principal in the KDC with the specified user and password.
* @param principal principal name, do not include the domain.
* @param password password.
* @throws Exception thrown if the principal could not be created.
*/
public synchronized void createPrincipal(String principal, String password) throws Exception {
this.simpleKdc.createPrincipal(principal, password);
}
/**
* Creates multiple principals in the KDC and adds them to a keytab file.
* @param keytabFile keytab file to add the created principals.
* @param principals principals to add to the KDC, do not include the domain.
* @throws Exception thrown if the principals or the keytab file could not be created.
*/
public synchronized void createPrincipal(File keytabFile, String... principals) throws Exception {
this.simpleKdc.createPrincipals(principals);
if (keytabFile.exists() && !keytabFile.delete()) {
LOG.error("Failed to delete keytab file: " + keytabFile);
}
for (String principal : principals) {
this.simpleKdc.getKadmin().exportKeytab(keytabFile, principal);
}
}
/**
* Set the System property; return the old value for caching.
* @param sysprop property
* @param debug true or false
* @return the previous value
*/
private boolean getAndSet(String sysprop, String debug) {
boolean old = Boolean.getBoolean(sysprop);
System.setProperty(sysprop, debug);
return old;
}
}
@@ -0,0 +1,192 @@
/*
* Copyright 2004-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.kerberos.test;
import java.io.File;
import java.security.Principal;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.security.auth.Subject;
import javax.security.auth.kerberos.KerberosPrincipal;
import javax.security.auth.login.AppConfigurationEntry;
import javax.security.auth.login.Configuration;
import javax.security.auth.login.LoginContext;
import org.apache.kerby.kerberos.kerb.keytab.Keytab;
import org.apache.kerby.kerberos.kerb.type.base.PrincipalName;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
public class TestMiniKdc extends KerberosSecurityTestcase {
private static final boolean IBM_JAVA = shouldUseIbmPackages();
// duplicated to avoid cycles in the build
private static boolean shouldUseIbmPackages() {
final List<String> ibmTechnologyEditionSecurityModules = Arrays.asList(
"com.ibm.security.auth.module.JAASLoginModule", "com.ibm.security.auth.module.Win64LoginModule",
"com.ibm.security.auth.module.NTLoginModule", "com.ibm.security.auth.module.AIX64LoginModule",
"com.ibm.security.auth.module.LinuxLoginModule", "com.ibm.security.auth.module.Krb5LoginModule");
if (System.getProperty("java.vendor").contains("IBM")) {
return ibmTechnologyEditionSecurityModules.stream().anyMatch((module) -> isSystemClassAvailable(module));
}
return false;
}
@Test
public void testKerberosLogin() throws Exception {
MiniKdc kdc = getKdc();
File workDir = getWorkDir();
LoginContext loginContext = null;
try {
String principal = "foo";
File keytab = new File(workDir, "foo.keytab");
kdc.createPrincipal(keytab, principal);
Set<Principal> principals = new HashSet<Principal>();
principals.add(new KerberosPrincipal(principal));
// client login
Subject subject = new Subject(false, principals, new HashSet<Object>(), new HashSet<Object>());
loginContext = new LoginContext("", subject, null,
KerberosConfiguration.createClientConfig(principal, keytab));
loginContext.login();
subject = loginContext.getSubject();
assertThat(subject.getPrincipals().size()).isEqualTo(1);
assertThat(subject.getPrincipals().iterator().next().getClass()).isEqualTo(KerberosPrincipal.class);
assertThat(subject.getPrincipals().iterator().next().getName()).isEqualTo(principal + "@" + kdc.getRealm());
loginContext.logout();
// server login
subject = new Subject(false, principals, new HashSet<Object>(), new HashSet<Object>());
loginContext = new LoginContext("", subject, null,
KerberosConfiguration.createServerConfig(principal, keytab));
loginContext.login();
subject = loginContext.getSubject();
assertThat(subject.getPrincipals().size()).isEqualTo(1);
assertThat(subject.getPrincipals().iterator().next().getClass()).isEqualTo(KerberosPrincipal.class);
assertThat(subject.getPrincipals().iterator().next().getName()).isEqualTo(principal + "@" + kdc.getRealm());
loginContext.logout();
}
finally {
if (loginContext != null && loginContext.getSubject() != null
&& !loginContext.getSubject().getPrivateCredentials().isEmpty()) {
loginContext.logout();
}
}
}
private static boolean isSystemClassAvailable(String className) {
try {
Class.forName(className);
return true;
}
catch (Exception ignored) {
return false;
}
}
@Test
public void testMiniKdcStart() {
MiniKdc kdc = getKdc();
assertThat(kdc.getPort()).isNotEqualTo(0);
}
@Test
public void testKeytabGen() throws Exception {
MiniKdc kdc = getKdc();
File workDir = getWorkDir();
kdc.createPrincipal(new File(workDir, "keytab"), "foo/bar", "bar/foo");
List<PrincipalName> principalNameList = Keytab.loadKeytab(new File(workDir, "keytab")).getPrincipals();
Set<String> principals = new HashSet<String>();
for (PrincipalName principalName : principalNameList) {
principals.add(principalName.getName());
}
assertThat(principals).containsExactlyInAnyOrder("foo/bar@" + kdc.getRealm(), "bar/foo@" + kdc.getRealm());
}
private static final class KerberosConfiguration extends Configuration {
private String principal;
private String keytab;
private boolean isInitiator;
private KerberosConfiguration(String principal, File keytab, boolean client) {
this.principal = principal;
this.keytab = keytab.getAbsolutePath();
this.isInitiator = client;
}
private static Configuration createClientConfig(String principal, File keytab) {
return new KerberosConfiguration(principal, keytab, true);
}
private static Configuration createServerConfig(String principal, File keytab) {
return new KerberosConfiguration(principal, keytab, false);
}
private static String getKrb5LoginModuleName() {
return System.getProperty("java.vendor").contains("IBM") ? "com.ibm.security.auth.module.Krb5LoginModule"
: "com.sun.security.auth.module.Krb5LoginModule";
}
@Override
public AppConfigurationEntry[] getAppConfigurationEntry(String name) {
Map<String, String> options = new HashMap<String, String>();
options.put("principal", this.principal);
options.put("refreshKrb5Config", "true");
if (IBM_JAVA) {
options.put("useKeytab", this.keytab);
options.put("credsType", "both");
}
else {
options.put("keyTab", this.keytab);
options.put("useKeyTab", "true");
options.put("storeKey", "true");
options.put("doNotPrompt", "true");
options.put("useTicketCache", "true");
options.put("renewTGT", "true");
options.put("isInitiator", Boolean.toString(this.isInitiator));
}
String ticketCache = System.getenv("KRB5CCNAME");
if (ticketCache != null) {
options.put("ticketCache", ticketCache);
}
options.put("debug", "true");
return new AppConfigurationEntry[] { new AppConfigurationEntry(getKrb5LoginModuleName(),
AppConfigurationEntry.LoginModuleControlFlag.REQUIRED, options) };
}
}
}
@@ -0,0 +1,10 @@
log4j.rootCategory=INFO, stdout
log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern=%d{ABSOLUTE} %5p %t %c{2} - %m%n
log4j.category.org.springframework.boot=INFO
xlog4j.category.org.apache.http.wire=TRACE
xlog4j.category.org.apache.http.headers=TRACE
@@ -0,0 +1,25 @@
#
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
[libdefaults]
default_realm = {0}
udp_preference_limit = 1
[realms]
{0} = '{'
kdc = {1}:{2}
'}'
@@ -0,0 +1,47 @@
#
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
dn: ou=users,dc=${0},dc=${1}
objectClass: organizationalUnit
objectClass: top
ou: users
dn: uid=krbtgt,ou=users,dc=${0},dc=${1}
objectClass: top
objectClass: person
objectClass: inetOrgPerson
objectClass: krb5principal
objectClass: krb5kdcentry
cn: KDC Service
sn: Service
uid: krbtgt
userPassword: secret
krb5PrincipalName: krbtgt/${2}.${3}@${2}.${3}
krb5KeyVersionNumber: 0
dn: uid=ldap,ou=users,dc=${0},dc=${1}
objectClass: top
objectClass: person
objectClass: inetOrgPerson
objectClass: krb5principal
objectClass: krb5kdcentry
cn: LDAP
sn: Service
uid: ldap
userPassword: secret
krb5PrincipalName: ldap/${4}@${2}.${3}
krb5KeyVersionNumber: 0
@@ -0,0 +1,19 @@
plugins {
id 'io.spring.convention.spring-module'
}
description = 'Spring Security Kerberos Web'
dependencies {
management platform(project(":spring-security-dependencies"))
implementation project(':spring-security-kerberos-core')
api(project(':spring-security-web'))
api(libs.jakarta.servlet.jakarta.servlet.api)
testImplementation 'org.springframework:spring-test'
testImplementation project(':spring-security-config')
testImplementation 'org.junit.jupiter:junit-jupiter'
testImplementation 'org.mockito:mockito-junit-jupiter'
testImplementation libs.org.assertj.assertj.core
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
}
@@ -0,0 +1,71 @@
/*
* Copyright 2004-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.kerberos.web.authentication;
import java.io.IOException;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.core.Authentication;
import org.springframework.security.kerberos.authentication.KerberosServiceRequestToken;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
/**
* Adds a WWW-Authenticate (or other) header to the response following successful
* authentication.
*
* @author Jeremy Stone
*/
public class ResponseHeaderSettingKerberosAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
private static final String NEGOTIATE_PREFIX = "Negotiate ";
private static final String WWW_AUTHENTICATE = "WWW-Authenticate";
private String headerName = WWW_AUTHENTICATE;
private String headerPrefix = NEGOTIATE_PREFIX;
/**
* Sets the name of the header to set. By default this is 'WWW-Authenticate'.
* @param headerName the www authenticate header name
*/
public void setHeaderName(String headerName) {
this.headerName = headerName;
}
/**
* Sets the value of the prefix for the encoded response token value. By default this
* is 'Negotiate '.
* @param headerPrefix the negotiate prefix
*/
public void setHeaderPrefix(String headerPrefix) {
this.headerPrefix = headerPrefix;
}
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
Authentication authentication) throws IOException, ServletException {
KerberosServiceRequestToken auth = (KerberosServiceRequestToken) authentication;
if (auth.hasResponseToken()) {
response.addHeader(this.headerName, this.headerPrefix + auth.getEncodedResponseToken());
}
}
}
@@ -0,0 +1,320 @@
/*
* Copyright 2004-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.kerberos.web.authentication;
import java.io.IOException;
import java.util.Base64;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.authentication.AnonymousAuthenticationToken;
import org.springframework.security.authentication.AuthenticationDetailsSource;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.context.SecurityContextHolderStrategy;
import org.springframework.security.kerberos.authentication.KerberosServiceAuthenticationProvider;
import org.springframework.security.kerberos.authentication.KerberosServiceRequestToken;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.security.web.authentication.session.NullAuthenticatedSessionStrategy;
import org.springframework.security.web.authentication.session.SessionAuthenticationStrategy;
import org.springframework.security.web.context.RequestAttributeSecurityContextRepository;
import org.springframework.security.web.context.SecurityContextRepository;
import org.springframework.util.Assert;
import org.springframework.web.filter.OncePerRequestFilter;
/**
* Parses the SPNEGO authentication Header, which was generated by the browser and creates
* a {@link KerberosServiceRequestToken} out if it. It will then call the
* {@link AuthenticationManager}.
*
* <p>
* A typical Spring Security configuration might look like this:
* </p>
*
* <pre>
* &lt;beans xmlns=&quot;https://www.springframework.org/schema/beans&quot;
* xmlns:xsi=&quot;https://www.w3.org/2001/XMLSchema-instance&quot; xmlns:sec=&quot;https://www.springframework.org/schema/security&quot;
* xsi:schemaLocation=&quot;https://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans-2.0.xsd
* https://www.springframework.org/schema/security https://www.springframework.org/schema/security/spring-security-3.0.xsd&quot;&gt;
*
* &lt;sec:http entry-point-ref=&quot;spnegoEntryPoint&quot;&gt;
* &lt;sec:intercept-url pattern=&quot;/secure/**&quot; access=&quot;IS_AUTHENTICATED_FULLY&quot; /&gt;
* &lt;sec:custom-filter ref=&quot;spnegoAuthenticationProcessingFilter&quot; position=&quot;BASIC_AUTH_FILTER&quot; /&gt;
* &lt;/sec:http&gt;
*
* &lt;bean id=&quot;spnegoEntryPoint&quot; class=&quot;org.springframework.security.kerberos.web.authentication.SpnegoEntryPoint&quot; /&gt;
*
* &lt;bean id=&quot;spnegoAuthenticationProcessingFilter&quot;
* class=&quot;org.springframework.security.kerberos.web.authentication.SpnegoAuthenticationProcessingFilter&quot;&gt;
* &lt;property name=&quot;authenticationManager&quot; ref=&quot;authenticationManager&quot; /&gt;
* &lt;/bean&gt;
*
* &lt;sec:authentication-manager alias=&quot;authenticationManager&quot;&gt;
* &lt;sec:authentication-provider ref=&quot;kerberosServiceAuthenticationProvider&quot; /&gt;
* &lt;/sec:authentication-manager&gt;
*
* &lt;bean id=&quot;kerberosServiceAuthenticationProvider&quot;
* class=&quot;org.springframework.security.kerberos.authenitcation.KerberosServiceAuthenticationProvider&quot;&gt;
* &lt;property name=&quot;ticketValidator&quot;&gt;
* &lt;bean class=&quot;org.springframework.security.kerberos.authentication.sun.SunJaasKerberosTicketValidator&quot;&gt;
* &lt;property name=&quot;servicePrincipal&quot; value=&quot;HTTP/web.springsource.com&quot; /&gt;
* &lt;property name=&quot;keyTabLocation&quot; value=&quot;classpath:http-java.keytab&quot; /&gt;
* &lt;/bean&gt;
* &lt;/property&gt;
* &lt;property name=&quot;userDetailsService&quot; ref=&quot;inMemoryUserDetailsService&quot; /&gt;
* &lt;/bean&gt;
*
* &lt;bean id=&quot;inMemoryUserDetailsService&quot;
* class=&quot;org.springframework.security.core.userdetails.memory.InMemoryDaoImpl&quot;&gt;
* &lt;property name=&quot;userProperties&quot;&gt;
* &lt;value&gt;
* mike@SECPOD.DE=notUsed,ROLE_ADMIN
* &lt;/value&gt;
* &lt;/property&gt;
* &lt;/bean&gt;
* &lt;/beans&gt;
* </pre>
*
* <p>
* If you get a "GSSException: Channel binding mismatch (Mechanism level:ChannelBinding
* not provided!) have a look at this
* <a href="https://bugs.sun.com/view_bug.do?bug_id=6851973">bug</a>.
* </p>
* <p>
* A workaround unti this is fixed in the JVM is to change
* </p>
* HKEY_LOCAL_MACHINE\System \CurrentControlSet\Control\LSA\SuppressExtendedProtection to
* 0x02
*
* @author Mike Wiesner
* @author Jeremy Stone
* @author Denis Angilella
* @since 1.0
* @see KerberosServiceAuthenticationProvider
* @see SpnegoEntryPoint
*/
public class SpnegoAuthenticationProcessingFilter extends OncePerRequestFilter {
private SecurityContextHolderStrategy securityContextHolderStrategy = SecurityContextHolder
.getContextHolderStrategy();
private SecurityContextRepository securityContextRepository = new RequestAttributeSecurityContextRepository();
private AuthenticationDetailsSource<HttpServletRequest, ?> authenticationDetailsSource = new WebAuthenticationDetailsSource();
private AuthenticationManager authenticationManager;
private AuthenticationSuccessHandler successHandler;
private AuthenticationFailureHandler failureHandler;
private SessionAuthenticationStrategy sessionStrategy = new NullAuthenticatedSessionStrategy();
private boolean skipIfAlreadyAuthenticated = true;
private boolean stopFilterChainOnSuccessfulAuthentication = false;
/**
* Authentication header prefix sent by IE/Windows when the domain controller fails to
* issue a Kerberos ticket for the URL.
*
* "TlRMTVNTUA" is the base64 encoding of "NTLMSSP". This will be followed by the
* actual token.
**/
private static final String NTLMSSP_PREFIX = "Negotiate TlRMTVNTUA";
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws ServletException, IOException {
if (this.skipIfAlreadyAuthenticated) {
Authentication existingAuth = SecurityContextHolder.getContext().getAuthentication();
if (existingAuth != null && existingAuth.isAuthenticated()
&& !(existingAuth instanceof AnonymousAuthenticationToken)) {
chain.doFilter(request, response);
return;
}
}
String header = request.getHeader("Authorization");
if (header != null && ((header.startsWith("Negotiate ") && !header.startsWith(NTLMSSP_PREFIX))
|| header.startsWith("Kerberos "))) {
if (this.logger.isDebugEnabled()) {
this.logger.debug("Received Negotiate Header for request " + request.getRequestURL() + ": " + header);
}
byte[] base64Token = header.substring(header.indexOf(" ") + 1).getBytes("UTF-8");
byte[] kerberosTicket = Base64.getDecoder().decode(base64Token);
KerberosServiceRequestToken authenticationRequest = new KerberosServiceRequestToken(kerberosTicket);
authenticationRequest.setDetails(this.authenticationDetailsSource.buildDetails(request));
Authentication authentication;
try {
authentication = this.authenticationManager.authenticate(authenticationRequest);
}
catch (AuthenticationException ex) {
// That shouldn't happen, as it is most likely a wrong
// configuration on the server side
this.logger.warn("Negotiate Header was invalid: " + header, ex);
this.securityContextHolderStrategy.clearContext();
if (this.failureHandler != null) {
this.failureHandler.onAuthenticationFailure(request, response, ex);
}
else {
response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
response.flushBuffer();
}
return;
}
this.sessionStrategy.onAuthentication(authentication, request, response);
SecurityContext context = this.securityContextHolderStrategy.createEmptyContext();
context.setAuthentication(authentication);
this.securityContextHolderStrategy.setContext(context);
this.securityContextRepository.saveContext(context, request, response);
if (this.successHandler != null) {
this.successHandler.onAuthenticationSuccess(request, response, authentication);
}
if (this.stopFilterChainOnSuccessfulAuthentication) {
return;
}
}
chain.doFilter(request, response);
}
@Override
public void afterPropertiesSet() throws ServletException {
super.afterPropertiesSet();
Assert.notNull(this.authenticationManager, "authenticationManager must be specified");
}
/**
* The authentication manager for validating the ticket.
* @param authenticationManager the authentication manager
*/
public void setAuthenticationManager(AuthenticationManager authenticationManager) {
this.authenticationManager = authenticationManager;
}
/**
* <p>
* This handler is called after a successful authentication. One can add additional
* authentication behavior by setting this.
* </p>
* <p>
* Default is null, which means nothing additional happens
* </p>
* @param successHandler the authentication success handler
*/
public void setSuccessHandler(AuthenticationSuccessHandler successHandler) {
this.successHandler = successHandler;
}
/**
* <p>
* This handler is called after a failure authentication. In most cases you only get
* Kerberos/SPNEGO failures with a wrong server or network configurations and not
* during runtime. If the client encounters an error, he will just stop the
* communication with server and therefore this handler will not be called in this
* case.
* </p>
* <p>
* Default is null, which means that the Filter returns the HTTP 500 code
* </p>
* @param failureHandler the authentication failure handler
*/
public void setFailureHandler(AuthenticationFailureHandler failureHandler) {
this.failureHandler = failureHandler;
}
/**
* Should Kerberos authentication be skipped if a user is already authenticated for
* this request (e.g. in the HTTP session).
* @param skipIfAlreadyAuthenticated default is true
*/
public void setSkipIfAlreadyAuthenticated(boolean skipIfAlreadyAuthenticated) {
this.skipIfAlreadyAuthenticated = skipIfAlreadyAuthenticated;
}
/**
* The session handling strategy which will be invoked immediately after an
* authentication request is successfully processed by the
* <tt>AuthenticationManager</tt>. Used, for example, to handle changing of the
* session identifier to prevent session fixation attacks.
* @param sessionStrategy the implementation to use. If not set a null implementation
* is used.
*/
public void setSessionAuthenticationStrategy(SessionAuthenticationStrategy sessionStrategy) {
this.sessionStrategy = sessionStrategy;
}
/**
* Sets the authentication details source.
* @param authenticationDetailsSource the authentication details source
*/
public void setAuthenticationDetailsSource(
AuthenticationDetailsSource<HttpServletRequest, ?> authenticationDetailsSource) {
Assert.notNull(authenticationDetailsSource, "AuthenticationDetailsSource required");
this.authenticationDetailsSource = authenticationDetailsSource;
}
/**
* If set to {@code false} (the default) and authentication is successful, the request
* will be processed by the next filter in the chain. If {@code true} and
* authentication is successful, the filter chain will stop here.
* @param shouldStop set to {@code true} to prevent the next filter in the chain from
* processing the request after a successful authentication.
* @since 1.0.2
*/
public void setStopFilterChainOnSuccessfulAuthentication(boolean shouldStop) {
this.stopFilterChainOnSuccessfulAuthentication = shouldStop;
}
/**
* Sets the {@link SecurityContextRepository} to save the {@link SecurityContext} on
* authentication success. The default action is not to save the
* {@link SecurityContext}.
* @param securityContextRepository the {@link SecurityContextRepository} to use.
* Cannot be null.
*/
public void setSecurityContextRepository(SecurityContextRepository securityContextRepository) {
Assert.notNull(securityContextRepository, "securityContextRepository cannot be null");
this.securityContextRepository = securityContextRepository;
}
/**
* Sets the {@link SecurityContextHolderStrategy} to use. The default action is to use
* the {@link SecurityContextHolderStrategy} stored in {@link SecurityContextHolder}.
* @param securityContextHolderStrategy the {@link SecurityContextHolderStrategy} to
* use. Cannot be null.
*/
public void setSecurityContextHolderStrategy(SecurityContextHolderStrategy securityContextHolderStrategy) {
Assert.notNull(securityContextHolderStrategy, "securityContextHolderStrategy cannot be null");
this.securityContextHolderStrategy = securityContextHolderStrategy;
}
}
@@ -0,0 +1,142 @@
/*
* Copyright 2004-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.kerberos.web.authentication;
import java.io.IOException;
import jakarta.servlet.RequestDispatcher;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletRequestWrapper;
import jakarta.servlet.http.HttpServletResponse;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.http.HttpMethod;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.security.web.util.UrlUtils;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
/**
* Sends back a request for a Negotiate Authentication to the browser.
*
* <p>
* With optional configured <code>forwardUrl</code> it is possible to use form login as
* fallback authentication.
* </p>
*
* <p>
* This approach enables security configuration to use SPNEGO in combination with login
* form as fallback for clients that do not support this kind of authentication. Set
* Response Code 401 - unauthorized and forward to login page. A useful scenario might be
* an environment where windows domain is present but it is required to access the
* application also from non domain client devices. One could use a combination with form
* based LDAP login.
* </p>
*
* <p>
* See <code>spnego-with-form-login.xml</code> in spring-security-kerberos-sample for
* details
* </p>
*
* @author Mike Wiesner
* @author Andre Schaefer, Namics AG
* @since 1.0
* @see SpnegoAuthenticationProcessingFilter
*/
public class SpnegoEntryPoint implements AuthenticationEntryPoint {
private static final Log LOG = LogFactory.getLog(SpnegoEntryPoint.class);
private final String forwardUrl;
private final HttpMethod forwardMethod;
private final boolean forward;
/**
* Instantiates a new spnego entry point. Using this constructor the EntryPoint will
* Sends back a request for a Negotiate Authentication to the browser without
* providing a fallback mechanism for login, Use constructor with forwardUrl to
* provide form based login.
*/
public SpnegoEntryPoint() {
this(null);
}
/**
* Instantiates a new spnego entry point. This constructor enables security
* configuration to use SPNEGO in combination with a fallback page (login form, custom
* 401 page ...). The forward method will be the same as the original request.
* @param forwardUrl URL where the login page can be found. Should be relative to the
* web-app context path (include a leading {@code /}) and can't be absolute URL.
*/
public SpnegoEntryPoint(String forwardUrl) {
this(forwardUrl, null);
}
/**
* Instantiates a new spnego entry point. This constructor enables security
* configuration to use SPNEGO in combination a fallback page (login form, custom 401
* page ...). The forward URL will be accessed via provided HTTP method.
* @param forwardUrl URL where the login page can be found. Should be relative to the
* web-app context path (include a leading {@code /}) and can't be absolute URL.
* @param forwardMethod HTTP method to use when accessing the forward URL
*/
public SpnegoEntryPoint(String forwardUrl, HttpMethod forwardMethod) {
if (StringUtils.hasText(forwardUrl)) {
Assert.isTrue(UrlUtils.isValidRedirectUrl(forwardUrl), "Forward url specified must be a valid forward URL");
Assert.isTrue(!UrlUtils.isAbsoluteUrl(forwardUrl), "Forward url specified must not be absolute");
this.forwardUrl = forwardUrl;
this.forwardMethod = forwardMethod;
this.forward = true;
}
else {
this.forwardUrl = null;
this.forwardMethod = null;
this.forward = false;
}
}
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException ex)
throws IOException, ServletException {
if (LOG.isDebugEnabled()) {
LOG.debug("Add header WWW-Authenticate:Negotiate to " + request.getRequestURL() + ", forward: "
+ (this.forward ? this.forwardUrl : "no"));
}
response.addHeader("WWW-Authenticate", "Negotiate");
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
if (this.forward) {
RequestDispatcher dispatcher = request.getRequestDispatcher(this.forwardUrl);
HttpServletRequest fwdRequest = (this.forwardMethod != null) ? new HttpServletRequestWrapper(request) {
@Override
public String getMethod() {
return SpnegoEntryPoint.this.forwardMethod.name();
}
} : request;
dispatcher.forward(fwdRequest, response);
}
else {
response.flushBuffer();
}
}
}
@@ -0,0 +1,44 @@
/*
* Copyright 2004-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.kerberos.docs;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.kerberos.authentication.KerberosAuthenticationProvider;
import org.springframework.security.kerberos.authentication.sun.SunJaasKerberosClient;
//tag::snippetA[]
@Configuration
public class AuthProviderConfig {
@Bean
public KerberosAuthenticationProvider kerberosAuthenticationProvider() {
KerberosAuthenticationProvider provider = new KerberosAuthenticationProvider();
SunJaasKerberosClient client = new SunJaasKerberosClient();
client.setDebug(true);
provider.setKerberosClient(client);
provider.setUserDetailsService(dummyUserDetailsService());
return provider;
}
@Bean
public DummyUserDetailsService dummyUserDetailsService() {
return new DummyUserDetailsService();
}
}
// end::snippetA[]
@@ -0,0 +1,33 @@
/*
* Copyright 2004-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.kerberos.docs;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit.jupiter.SpringExtension;
@ExtendWith(SpringExtension.class)
@ContextConfiguration(locations = { "AuthProviderConfig.xml" })
public class AuthProviderConfigTests {
@Test
public void configLoads() {
}
}
@@ -0,0 +1,34 @@
/*
* Copyright 2004-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.kerberos.docs;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
//tag::snippetA[]
public class DummyUserDetailsService implements UserDetailsService {
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
return new User(username, "notUsed", true, true, true, true, AuthorityUtils.createAuthorityList("ROLE_USER"));
}
}
// end::snippetA[]
@@ -0,0 +1,80 @@
/*
* Copyright 2004-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.kerberos.docs;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.FileSystemResource;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.kerberos.authentication.KerberosAuthenticationProvider;
import org.springframework.security.kerberos.authentication.KerberosServiceAuthenticationProvider;
import org.springframework.security.kerberos.authentication.sun.SunJaasKerberosClient;
import org.springframework.security.kerberos.authentication.sun.SunJaasKerberosTicketValidator;
import org.springframework.security.kerberos.web.authentication.SpnegoAuthenticationProcessingFilter;
import org.springframework.security.kerberos.web.authentication.SpnegoEntryPoint;
//tag::snippetA[]
@Configuration
public class SpnegoConfig {
@Bean
public KerberosAuthenticationProvider kerberosAuthenticationProvider() {
KerberosAuthenticationProvider provider = new KerberosAuthenticationProvider();
SunJaasKerberosClient client = new SunJaasKerberosClient();
client.setDebug(true);
provider.setKerberosClient(client);
provider.setUserDetailsService(dummyUserDetailsService());
return provider;
}
@Bean
public SpnegoEntryPoint spnegoEntryPoint() {
return new SpnegoEntryPoint("/login");
}
@Bean
public SpnegoAuthenticationProcessingFilter spnegoAuthenticationProcessingFilter(
AuthenticationManager authenticationManager) {
SpnegoAuthenticationProcessingFilter filter = new SpnegoAuthenticationProcessingFilter();
filter.setAuthenticationManager(authenticationManager);
return filter;
}
@Bean
public KerberosServiceAuthenticationProvider kerberosServiceAuthenticationProvider() {
KerberosServiceAuthenticationProvider provider = new KerberosServiceAuthenticationProvider();
provider.setTicketValidator(sunJaasKerberosTicketValidator());
provider.setUserDetailsService(dummyUserDetailsService());
return provider;
}
@Bean
public SunJaasKerberosTicketValidator sunJaasKerberosTicketValidator() {
SunJaasKerberosTicketValidator ticketValidator = new SunJaasKerberosTicketValidator();
ticketValidator.setServicePrincipal("HTTP/servicehost.example.org@EXAMPLE.ORG");
ticketValidator.setKeyTabLocation(new FileSystemResource("/tmp/service.keytab"));
ticketValidator.setDebug(true);
return ticketValidator;
}
@Bean
public DummyUserDetailsService dummyUserDetailsService() {
return new DummyUserDetailsService();
}
}
// end::snippetA[]
@@ -0,0 +1,298 @@
/*
* Copyright 2004-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.kerberos.web;
import java.io.IOException;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.security.authentication.AnonymousAuthenticationToken;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.kerberos.authentication.KerberosServiceRequestToken;
import org.springframework.security.kerberos.authentication.KerberosTicketValidation;
import org.springframework.security.kerberos.web.authentication.SpnegoAuthenticationProcessingFilter;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.security.web.context.SecurityContextRepository;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
/**
* Test class for {@link SpnegoAuthenticationProcessingFilter}
*
* @author Mike Wiesner
* @author Jeremy Stone
* @since 1.0
*/
public class SpnegoAuthenticationProcessingFilterTests {
private SpnegoAuthenticationProcessingFilter filter;
private AuthenticationManager authenticationManager;
private HttpServletRequest request;
private HttpServletResponse response;
private FilterChain chain;
private AuthenticationSuccessHandler successHandler;
private AuthenticationFailureHandler failureHandler;
private WebAuthenticationDetailsSource detailsSource;
// data
private static final byte[] TEST_TOKEN = "TestToken".getBytes();
private static final String TEST_TOKEN_BASE64 = "VGVzdFRva2Vu";
private static KerberosTicketValidation UNUSED_TICKET_VALIDATION = mock(KerberosTicketValidation.class);
private static final Authentication AUTHENTICATION = new KerberosServiceRequestToken("test",
UNUSED_TICKET_VALIDATION, AuthorityUtils.createAuthorityList("ROLE_ADMIN"), TEST_TOKEN);
private static final String HEADER = "Authorization";
private static final String TOKEN_PREFIX_NEG = "Negotiate ";
private static final String TOKEN_PREFIX_KERB = "Kerberos ";
private static final String TOKEN_NTLM = "Negotiate TlRMTVNTUAABAAAAl4II4gAAAAAAAAAAAAAAAAAAAAAGAbEdAAAADw==";
private static final BadCredentialsException BCE = new BadCredentialsException("");
@BeforeEach
public void before() throws Exception {
// mocking
this.authenticationManager = mock(AuthenticationManager.class);
this.detailsSource = new WebAuthenticationDetailsSource();
this.filter = new SpnegoAuthenticationProcessingFilter();
this.filter.setAuthenticationManager(this.authenticationManager);
this.request = mock(HttpServletRequest.class);
this.response = mock(HttpServletResponse.class);
this.chain = mock(FilterChain.class);
this.filter.afterPropertiesSet();
}
@Test
public void testEverythingWorks() throws Exception {
everythingWorks(TOKEN_PREFIX_NEG);
}
@Test
public void testEverythingWorks_Kerberos() throws Exception {
everythingWorks(TOKEN_PREFIX_KERB);
}
@Test
public void testEverythingWorksWithHandlers() throws Exception {
everythingWorksWithHandlers(TOKEN_PREFIX_NEG);
}
@Test
public void testEverythingWorksWithHandlers_Kerberos() throws Exception {
everythingWorksWithHandlers(TOKEN_PREFIX_KERB);
}
private void everythingWorksWithHandlers(String tokenPrefix) throws Exception {
createHandler();
everythingWorks(tokenPrefix);
everythingWorksVerifyHandlers();
}
private void everythingWorksVerifyHandlers() throws Exception {
verify(this.successHandler).onAuthenticationSuccess(this.request, this.response, AUTHENTICATION);
verify(this.failureHandler, never()).onAuthenticationFailure(any(HttpServletRequest.class),
any(HttpServletResponse.class), any(AuthenticationException.class));
}
private void everythingWorks(String tokenPrefix) throws IOException, ServletException {
// stubbing
SecurityContextRepository securityContextRepository = mock(SecurityContextRepository.class);
this.filter.setSecurityContextRepository(securityContextRepository);
everythingWorksStub(tokenPrefix);
// testing
this.filter.doFilter(this.request, this.response, this.chain);
verify(this.chain).doFilter(this.request, this.response);
verify(securityContextRepository).saveContext(SecurityContextHolder.getContext(), this.request, this.response);
assertThat(SecurityContextHolder.getContext().getAuthentication()).isEqualTo(AUTHENTICATION);
}
@Test
public void testNoHeader() throws Exception {
this.filter.doFilter(this.request, this.response, this.chain);
// If the header is not present, the filter is not allowed to call
// authenticate()
verify(this.authenticationManager, never()).authenticate(any(Authentication.class));
// chain should go on
verify(this.chain).doFilter(this.request, this.response);
assertThat(SecurityContextHolder.getContext().getAuthentication()).isEqualTo(null);
}
@Test
public void testNTLMSSPHeader() throws Exception {
given(this.request.getHeader(HEADER)).willReturn(TOKEN_NTLM);
this.filter.doFilter(this.request, this.response, this.chain);
// If the header is not present, the filter is not allowed to call
// authenticate()
verify(this.authenticationManager, never()).authenticate(any(Authentication.class));
// chain should go on
verify(this.chain).doFilter(this.request, this.response);
assertThat(SecurityContextHolder.getContext().getAuthentication()).isEqualTo(null);
}
@Test
public void testAuthenticationFails() throws Exception {
authenticationFails();
verify(this.response).setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
}
@Test
public void testAuthenticationFailsWithHandlers() throws Exception {
createHandler();
authenticationFails();
verify(this.failureHandler).onAuthenticationFailure(this.request, this.response, BCE);
verify(this.successHandler, never()).onAuthenticationSuccess(any(HttpServletRequest.class),
any(HttpServletResponse.class), any(Authentication.class));
verify(this.response, never()).setStatus(anyInt());
}
@Test
public void testAlreadyAuthenticated() throws Exception {
try {
Authentication existingAuth = new UsernamePasswordAuthenticationToken("mike", "mike",
AuthorityUtils.createAuthorityList("ROLE_TEST"));
SecurityContextHolder.getContext().setAuthentication(existingAuth);
given(this.request.getHeader(HEADER)).willReturn(TOKEN_PREFIX_NEG + TEST_TOKEN_BASE64);
this.filter.doFilter(this.request, this.response, this.chain);
verify(this.authenticationManager, never()).authenticate(any(Authentication.class));
}
finally {
SecurityContextHolder.clearContext();
}
}
@Test
public void testAlreadyAuthenticatedWithNotAuthenticatedToken() throws Exception {
try {
// this token is not authenticated yet!
Authentication existingAuth = new UsernamePasswordAuthenticationToken("mike", "mike");
SecurityContextHolder.getContext().setAuthentication(existingAuth);
everythingWorks(TOKEN_PREFIX_NEG);
}
finally {
SecurityContextHolder.clearContext();
}
}
@Test
public void testAlreadyAuthenticatedWithAnonymousToken() throws Exception {
try {
Authentication existingAuth = new AnonymousAuthenticationToken("test", "mike",
AuthorityUtils.createAuthorityList("ROLE_TEST"));
SecurityContextHolder.getContext().setAuthentication(existingAuth);
everythingWorks(TOKEN_PREFIX_NEG);
}
finally {
SecurityContextHolder.clearContext();
}
}
@Test
public void testAlreadyAuthenticatedNotActive() throws Exception {
try {
Authentication existingAuth = new UsernamePasswordAuthenticationToken("mike", "mike",
AuthorityUtils.createAuthorityList("ROLE_TEST"));
SecurityContextHolder.getContext().setAuthentication(existingAuth);
this.filter.setSkipIfAlreadyAuthenticated(false);
everythingWorks(TOKEN_PREFIX_NEG);
}
finally {
SecurityContextHolder.clearContext();
}
}
@Test
public void testEverythingWorksWithHandlers_stopFilterChain() throws Exception {
this.filter.setStopFilterChainOnSuccessfulAuthentication(true);
createHandler();
everythingWorksStub(TOKEN_PREFIX_NEG);
// testing
this.filter.doFilter(this.request, this.response, this.chain);
verify(this.chain, never()).doFilter(this.request, this.response);
assertThat(SecurityContextHolder.getContext().getAuthentication()).isEqualTo(AUTHENTICATION);
everythingWorksVerifyHandlers();
}
private void everythingWorksStub(String tokenPrefix) throws IOException, ServletException {
given(this.request.getHeader(HEADER)).willReturn(tokenPrefix + TEST_TOKEN_BASE64);
KerberosServiceRequestToken requestToken = new KerberosServiceRequestToken(TEST_TOKEN);
requestToken.setDetails(this.detailsSource.buildDetails(this.request));
given(this.authenticationManager.authenticate(requestToken)).willReturn(AUTHENTICATION);
}
private void authenticationFails() throws IOException, ServletException {
// stubbing
given(this.request.getHeader(HEADER)).willReturn(TOKEN_PREFIX_NEG + TEST_TOKEN_BASE64);
given(this.authenticationManager.authenticate(any(Authentication.class))).willThrow(BCE);
// testing
this.filter.doFilter(this.request, this.response, this.chain);
// chain should stop here and it should send back a 500
// future version should call some error handler
verify(this.chain, never()).doFilter(any(ServletRequest.class), any(ServletResponse.class));
}
private void createHandler() {
this.successHandler = mock(AuthenticationSuccessHandler.class);
this.failureHandler = mock(AuthenticationFailureHandler.class);
this.filter.setSuccessHandler(this.successHandler);
this.filter.setFailureHandler(this.failureHandler);
}
@AfterEach
public void after() {
SecurityContextHolder.clearContext();
}
}
@@ -0,0 +1,121 @@
/*
* Copyright 2004-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.kerberos.web;
import jakarta.servlet.RequestDispatcher;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
import org.springframework.http.HttpMethod;
import org.springframework.security.kerberos.web.authentication.SpnegoEntryPoint;
import org.springframework.web.bind.annotation.RequestMethod;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
/**
* Test class for {@link SpnegoEntryPoint}
*
* @author Mike Wiesner
* @author Janne Valkealahti
* @author Andre Schaefer, Namics AG
* @since 1.0
*/
public class SpnegoEntryPointTests {
private SpnegoEntryPoint entryPoint = new SpnegoEntryPoint();
@Test
public void testEntryPointOk() throws Exception {
HttpServletRequest request = mock(HttpServletRequest.class);
HttpServletResponse response = mock(HttpServletResponse.class);
this.entryPoint.commence(request, response, null);
verify(response).addHeader("WWW-Authenticate", "Negotiate");
verify(response).setStatus(HttpServletResponse.SC_UNAUTHORIZED);
}
@Test
public void testEntryPointOkWithDispatcher() throws Exception {
SpnegoEntryPoint entryPoint = new SpnegoEntryPoint();
HttpServletResponse response = mock(HttpServletResponse.class);
HttpServletRequest request = mock(HttpServletRequest.class);
RequestDispatcher requestDispatcher = mock(RequestDispatcher.class);
given(request.getRequestDispatcher(anyString())).willReturn(requestDispatcher);
entryPoint.commence(request, response, null);
verify(response).addHeader("WWW-Authenticate", "Negotiate");
verify(response).setStatus(HttpServletResponse.SC_UNAUTHORIZED);
}
@Test
public void testEntryPointForwardOk() throws Exception {
String forwardUrl = "/login";
SpnegoEntryPoint entryPoint = new SpnegoEntryPoint(forwardUrl);
HttpServletResponse response = mock(HttpServletResponse.class);
HttpServletRequest request = mock(HttpServletRequest.class);
RequestDispatcher requestDispatcher = mock(RequestDispatcher.class);
given(request.getRequestDispatcher(anyString())).willReturn(requestDispatcher);
entryPoint.commence(request, response, null);
verify(response).addHeader("WWW-Authenticate", "Negotiate");
verify(response).setStatus(HttpServletResponse.SC_UNAUTHORIZED);
verify(request).getRequestDispatcher(forwardUrl);
verify(requestDispatcher).forward(request, response);
}
@Test
public void testForwardUsesDefaultHttpMethod() throws Exception {
ArgumentCaptor<HttpServletRequest> servletRequestCaptor = ArgumentCaptor.forClass(HttpServletRequest.class);
String forwardUrl = "/login";
SpnegoEntryPoint entryPoint = new SpnegoEntryPoint(forwardUrl);
HttpServletResponse response = mock(HttpServletResponse.class);
HttpServletRequest request = mock(HttpServletRequest.class);
given(request.getMethod()).willReturn(RequestMethod.POST.name());
RequestDispatcher requestDispatcher = mock(RequestDispatcher.class);
given(request.getRequestDispatcher(anyString())).willReturn(requestDispatcher);
entryPoint.commence(request, response, null);
verify(requestDispatcher).forward(servletRequestCaptor.capture(), eq(response));
assertThat(servletRequestCaptor.getValue().getMethod()).isEqualTo(HttpMethod.POST.name());
}
@Test
public void testForwardUsesCustomHttpMethod() throws Exception {
ArgumentCaptor<HttpServletRequest> servletRequestCaptor = ArgumentCaptor.forClass(HttpServletRequest.class);
String forwardUrl = "/login";
SpnegoEntryPoint entryPoint = new SpnegoEntryPoint(forwardUrl, HttpMethod.DELETE);
HttpServletResponse response = mock(HttpServletResponse.class);
HttpServletRequest request = mock(HttpServletRequest.class);
RequestDispatcher requestDispatcher = mock(RequestDispatcher.class);
given(request.getRequestDispatcher(anyString())).willReturn(requestDispatcher);
entryPoint.commence(request, response, null);
verify(requestDispatcher).forward(servletRequestCaptor.capture(), eq(response));
assertThat(servletRequestCaptor.getValue().getMethod()).isEqualTo(HttpMethod.DELETE.name());
}
@Test
public void testEntryPointForwardAbsolute() throws Exception {
assertThatIllegalArgumentException().isThrownBy(() -> new SpnegoEntryPoint("http://test/login"));
}
}
@@ -0,0 +1,47 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- tag::snippetA[] -->
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:sec="http://www.springframework.org/schema/security"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="
http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context-3.2.xsd
http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans-3.2.xsd
http://www.springframework.org/schema/security https://www.springframework.org/schema/security/spring-security.xsd">
<sec:http entry-point-ref="spnegoEntryPoint" use-expressions="true">
<sec:intercept-url pattern="/" access="permitAll" />
<sec:intercept-url pattern="/home" access="permitAll" />
<sec:intercept-url pattern="/**" access="authenticated"/>
</sec:http>
<sec:authentication-manager alias="authenticationManager">
<sec:authentication-provider ref="kerberosAuthenticationProvider"/>
</sec:authentication-manager>
<bean id="kerberosAuthenticationProvider"
class="org.springframework.security.kerberos.authentication.KerberosAuthenticationProvider">
<property name="kerberosClient">
<bean class="org.springframework.security.kerberos.authentication.sun.SunJaasKerberosClient">
<property name="debug" value="true"/>
</bean>
</property>
<property name="userDetailsService" ref="dummyUserDetailsService"/>
</bean>
<bean
class="org.springframework.security.kerberos.authentication.sun.GlobalSunJaasKerberosConfig">
<property name="debug" value="true" />
<property name="krbConfLocation" value="/path/to/krb5.ini"/>
</bean>
<bean id="dummyUserDetailsService"
class="org.springframework.security.kerberos.docs.DummyUserDetailsService" />
<bean id="spnegoEntryPoint"
class="org.springframework.security.kerberos.web.authentication.SpnegoEntryPoint" >
<constructor-arg value="/login" />
</bean>
</beans>
<!-- end::snippetA[] -->
@@ -0,0 +1,63 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- tag::snippetA[] -->
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:sec="http://www.springframework.org/schema/security"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/security https://www.springframework.org/schema/security/spring-security.xsd
http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans-4.1.xsd
http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context-4.1.xsd">
<sec:http entry-point-ref="spnegoEntryPoint" use-expressions="true" >
<sec:intercept-url pattern="/" access="permitAll" />
<sec:intercept-url pattern="/home" access="permitAll" />
<sec:intercept-url pattern="/login" access="permitAll" />
<sec:intercept-url pattern="/**" access="authenticated"/>
<sec:form-login login-page="/login" />
<sec:custom-filter ref="spnegoAuthenticationProcessingFilter"
before="BASIC_AUTH_FILTER" />
</sec:http>
<sec:authentication-manager alias="authenticationManager">
<sec:authentication-provider ref="kerberosAuthenticationProvider" />
<sec:authentication-provider ref="kerberosServiceAuthenticationProvider" />
</sec:authentication-manager>
<bean id="kerberosAuthenticationProvider"
class="org.springframework.security.kerberos.authentication.KerberosAuthenticationProvider">
<property name="userDetailsService" ref="dummyUserDetailsService"/>
<property name="kerberosClient">
<bean class="org.springframework.security.kerberos.authentication.sun.SunJaasKerberosClient">
<property name="debug" value="true"/>
</bean>
</property>
</bean>
<bean id="spnegoEntryPoint"
class="org.springframework.security.kerberos.web.authentication.SpnegoEntryPoint" >
<constructor-arg value="/login" />
</bean>
<bean id="spnegoAuthenticationProcessingFilter"
class="org.springframework.security.kerberos.web.authentication.SpnegoAuthenticationProcessingFilter">
<property name="authenticationManager" ref="authenticationManager" />
</bean>
<bean id="kerberosServiceAuthenticationProvider"
class="org.springframework.security.kerberos.authentication.KerberosServiceAuthenticationProvider">
<property name="ticketValidator">
<bean
class="org.springframework.security.kerberos.authentication.sun.SunJaasKerberosTicketValidator">
<property name="servicePrincipal" value="${app.service-principal}" />
<property name="keyTabLocation" value="${app.keytab-location}" />
<property name="debug" value="true" />
</bean>
</property>
<property name="userDetailsService" ref="dummyUserDetailsService" />
</bean>
<bean id="dummyUserDetailsService"
class="org.springframework.security.kerberos.docs.DummyUserDetailsService" />
</beans>
<!-- end::snippetA[] -->
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:util="http://www.springframework.org/schema/util"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context-4.1.xsd
http://www.springframework.org/schema/util https://www.springframework.org/schema/util/spring-util-4.1.xsd">
<context:property-placeholder location="app.properties"/>
</beans>