Null safety via JSpecify spring-security-kerberos-core

Closes gh-18549
This commit is contained in:
Robert Winch 2026-01-21 17:29:59 -06:00
parent b970746a03
commit f942ead2eb
No known key found for this signature in database
11 changed files with 149 additions and 43 deletions

View File

@ -1,4 +1,5 @@
plugins {
id 'security-nullability'
id 'io.spring.convention.spring-module'
id 'javadoc-warnings-error'
}

View File

@ -22,6 +22,8 @@ import java.util.Map;
import javax.security.auth.Subject;
import org.jspecify.annotations.Nullable;
import org.springframework.security.kerberos.authentication.sun.SunJaasKerberosClient;
/**
@ -40,7 +42,7 @@ public class JaasSubjectHolder implements Serializable {
private Subject jaasSubject;
private String username;
private @Nullable String username;
private Map<String, byte[]> savedTokens = new HashMap<String, byte[]>();
@ -53,7 +55,7 @@ public class JaasSubjectHolder implements Serializable {
this.username = username;
}
public String getUsername() {
public @Nullable String getUsername() {
return this.username;
}
@ -65,7 +67,7 @@ public class JaasSubjectHolder implements Serializable {
this.savedTokens.put(targetService, outToken);
}
public byte[] getToken(String principalName) {
public byte @Nullable [] getToken(String principalName) {
return this.savedTokens.get(principalName);
}

View File

@ -16,6 +16,8 @@
package org.springframework.security.kerberos.authentication;
import org.jspecify.annotations.Nullable;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
@ -32,17 +34,31 @@ import org.springframework.security.core.userdetails.UserDetailsService;
*/
public class KerberosAuthenticationProvider implements AuthenticationProvider {
private KerberosClient kerberosClient;
private @Nullable KerberosClient kerberosClient;
private UserDetailsService userDetailsService;
private @Nullable 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());
if (this.kerberosClient == null) {
throw new IllegalStateException("kerberosClient must be set");
}
if (this.userDetailsService == null) {
throw new IllegalStateException("userDetailsService must be set");
}
Object credentials = auth.getCredentials();
if (credentials == null) {
throw new IllegalArgumentException("credentials cannot be null");
}
JaasSubjectHolder subjectHolder = this.kerberosClient.login(auth.getName(), credentials.toString());
String username = subjectHolder.getUsername();
if (username == null) {
throw new IllegalStateException("username cannot be null");
}
UserDetails userDetails = this.userDetailsService.loadUserByUsername(username);
KerberosUsernamePasswordAuthenticationToken output = new KerberosUsernamePasswordAuthenticationToken(
userDetails, auth.getCredentials(), userDetails.getAuthorities(), subjectHolder);
userDetails, credentials, userDetails.getAuthorities(), subjectHolder);
output.setDetails(authentication.getDetails());
return output;

View File

@ -26,6 +26,7 @@ import org.ietf.jgss.GSSException;
import org.ietf.jgss.GSSManager;
import org.ietf.jgss.GSSName;
import org.ietf.jgss.Oid;
import org.jspecify.annotations.Nullable;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.core.Authentication;
@ -59,9 +60,9 @@ public final class KerberosMultiTier {
final JaasSubjectHolder jaasSubjectHolder = kerberosAuthentication.getJaasSubjectHolder();
Subject subject = jaasSubjectHolder.getJaasSubject();
Subject.doAs(subject, new PrivilegedAction<Object>() {
Subject.doAs(subject, new PrivilegedAction<@Nullable Object>() {
@Override
public Object run() {
public @Nullable Object run() {
runAuthentication(jaasSubjectHolder, username, lifetimeInSeconds, targetService);
return null;
@ -71,7 +72,7 @@ public final class KerberosMultiTier {
return authentication;
}
public static byte[] getTokenForService(Authentication authentication, String principalName) {
public static byte @Nullable [] getTokenForService(Authentication authentication, String principalName) {
KerberosAuthentication kerberosAuthentication = (KerberosAuthentication) authentication;
final JaasSubjectHolder jaasSubjectHolder = kerberosAuthentication.getJaasSubjectHolder();

View File

@ -18,6 +18,7 @@ package org.springframework.security.kerberos.authentication;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.jspecify.annotations.Nullable;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.security.authentication.AccountStatusUserDetailsChecker;
@ -55,9 +56,9 @@ public class KerberosServiceAuthenticationProvider implements AuthenticationProv
private static final Log LOG = LogFactory.getLog(KerberosServiceAuthenticationProvider.class);
private KerberosTicketValidator ticketValidator;
private @Nullable KerberosTicketValidator ticketValidator;
private UserDetailsService userDetailsService;
private @Nullable UserDetailsService userDetailsService;
private UserDetailsChecker userDetailsChecker = new AccountStatusUserDetailsChecker();
@ -66,6 +67,12 @@ public class KerberosServiceAuthenticationProvider implements AuthenticationProv
KerberosServiceRequestToken auth = (KerberosServiceRequestToken) authentication;
byte[] token = auth.getToken();
LOG.debug("Try to validate Kerberos Token");
if (this.ticketValidator == null) {
throw new IllegalStateException("ticketValidator must be set");
}
if (this.userDetailsService == null) {
throw new IllegalStateException("userDetailsService must be set");
}
KerberosTicketValidation ticketValidation = this.ticketValidator.validateTicket(token);
LOG.debug("Successfully validated " + ticketValidation.username());
UserDetails userDetails = this.userDetailsService.loadUserByUsername(ticketValidation.username());

View File

@ -26,6 +26,7 @@ import javax.security.auth.Subject;
import org.ietf.jgss.GSSContext;
import org.ietf.jgss.MessageProp;
import org.jspecify.annotations.Nullable;
import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;
@ -56,11 +57,11 @@ public class KerberosServiceRequestToken extends AbstractAuthenticationToken imp
private final byte[] token;
private final Object principal;
private final @Nullable Object principal;
private final transient KerberosTicketValidation ticketValidation;
private final transient @Nullable KerberosTicketValidation ticketValidation;
private JaasSubjectHolder jaasSubjectHolder;
private @Nullable JaasSubjectHolder jaasSubjectHolder;
/**
* Creates an authenticated token, normally used as an output of an authentication
@ -127,12 +128,12 @@ public class KerberosServiceRequestToken extends AbstractAuthenticationToken imp
}
@Override
public Object getCredentials() {
public @Nullable Object getCredentials() {
return null;
}
@Override
public Object getPrincipal() {
public @Nullable Object getPrincipal() {
return this.principal;
}
@ -148,7 +149,7 @@ public class KerberosServiceRequestToken extends AbstractAuthenticationToken imp
* Gets the ticket validation
* @return the ticket validation (which will be null if the token is unauthenticated)
*/
public KerberosTicketValidation getTicketValidation() {
public @Nullable KerberosTicketValidation getTicketValidation() {
return this.ticketValidation;
}
@ -168,6 +169,9 @@ public class KerberosServiceRequestToken extends AbstractAuthenticationToken imp
if (!hasResponseToken()) {
throw new IllegalStateException("Unauthenticated or no response token");
}
if (this.ticketValidation == null) {
throw new IllegalStateException("Ticket validation is not available");
}
return Base64.getEncoder().encodeToString(this.ticketValidation.responseToken());
}
@ -180,9 +184,16 @@ public class KerberosServiceRequestToken extends AbstractAuthenticationToken imp
* @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[]>() {
KerberosTicketValidation validation = getTicketValidation();
if (validation == null) {
throw new IllegalStateException("Cannot decrypt without ticket validation");
}
return Subject.doAs(validation.subject(), new PrivilegedExceptionAction<byte[]>() {
public byte[] run() throws Exception {
final GSSContext context = getTicketValidation().getGssContext();
final GSSContext context = validation.getGssContext();
if (context == null) {
throw new IllegalStateException("GSSContext is not available");
}
return context.unwrap(data, offset, length, new MessageProp(true));
}
});
@ -207,9 +218,16 @@ public class KerberosServiceRequestToken extends AbstractAuthenticationToken imp
* @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[]>() {
KerberosTicketValidation validation = getTicketValidation();
if (validation == null) {
throw new IllegalStateException("Cannot encrypt without ticket validation");
}
return Subject.doAs(validation.subject(), new PrivilegedExceptionAction<byte[]>() {
public byte[] run() throws Exception {
final GSSContext context = getTicketValidation().getGssContext();
final GSSContext context = validation.getGssContext();
if (context == null) {
throw new IllegalStateException("GSSContext is not available");
}
return context.wrap(data, offset, length, new MessageProp(true));
}
});
@ -227,6 +245,9 @@ public class KerberosServiceRequestToken extends AbstractAuthenticationToken imp
@Override
public JaasSubjectHolder getJaasSubjectHolder() {
if (this.jaasSubjectHolder == null) {
throw new IllegalStateException("JaasSubjectHolder is not available for unauthenticated token");
}
return this.jaasSubjectHolder;
}

View File

@ -23,6 +23,7 @@ import javax.security.auth.kerberos.KerberosPrincipal;
import org.ietf.jgss.GSSContext;
import org.ietf.jgss.GSSCredential;
import org.jspecify.annotations.Nullable;
/**
* Result of ticket validation
@ -35,17 +36,17 @@ public final class KerberosTicketValidation {
private final byte[] responseToken;
private final GSSContext gssContext;
private final @Nullable GSSContext gssContext;
private final GSSCredential delegationCredential;
private final @Nullable GSSCredential delegationCredential;
public KerberosTicketValidation(String username, String servicePrincipal, byte[] responseToken,
GSSContext gssContext) {
@Nullable GSSContext gssContext) {
this(username, servicePrincipal, responseToken, gssContext, null);
}
public KerberosTicketValidation(String username, String servicePrincipal, byte[] responseToken,
GSSContext gssContext, GSSCredential delegationCredential) {
@Nullable GSSContext gssContext, @Nullable GSSCredential delegationCredential) {
final HashSet<KerberosPrincipal> princs = new HashSet<KerberosPrincipal>();
princs.add(new KerberosPrincipal(servicePrincipal));
@ -56,12 +57,13 @@ public final class KerberosTicketValidation {
this.delegationCredential = delegationCredential;
}
public KerberosTicketValidation(String username, Subject subject, byte[] responseToken, GSSContext gssContext) {
public KerberosTicketValidation(String username, Subject subject, byte[] responseToken,
@Nullable GSSContext gssContext) {
this(username, subject, responseToken, gssContext, null);
}
public KerberosTicketValidation(String username, Subject subject, byte[] responseToken, GSSContext gssContext,
GSSCredential delegationCredential) {
public KerberosTicketValidation(String username, Subject subject, byte[] responseToken,
@Nullable GSSContext gssContext, @Nullable GSSCredential delegationCredential) {
this.username = username;
this.subject = subject;
this.responseToken = responseToken;
@ -77,7 +79,7 @@ public final class KerberosTicketValidation {
return this.responseToken;
}
public GSSContext getGssContext() {
public @Nullable GSSContext getGssContext() {
return this.gssContext;
}
@ -85,7 +87,7 @@ public final class KerberosTicketValidation {
return this.subject;
}
public GSSCredential getDelegationCredential() {
public @Nullable GSSCredential getDelegationCredential() {
return this.delegationCredential;
}

View File

@ -0,0 +1,20 @@
/*
* 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.
*/
@NullMarked
package org.springframework.security.kerberos.authentication;
import org.jspecify.annotations.NullMarked;

View File

@ -16,6 +16,8 @@
package org.springframework.security.kerberos.authentication.sun;
import org.jspecify.annotations.Nullable;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.config.BeanPostProcessor;
@ -30,7 +32,7 @@ public class GlobalSunJaasKerberosConfig implements BeanPostProcessor, Initializ
private boolean debug = false;
private String krbConfLocation;
private @Nullable String krbConfLocation;
@Override
public void afterPropertiesSet() throws Exception {

View File

@ -36,6 +36,7 @@ import org.ietf.jgss.GSSContext;
import org.ietf.jgss.GSSCredential;
import org.ietf.jgss.GSSManager;
import org.ietf.jgss.GSSName;
import org.jspecify.annotations.Nullable;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.core.io.ClassPathResource;
@ -58,13 +59,13 @@ import org.springframework.util.Assert;
*/
public class SunJaasKerberosTicketValidator implements KerberosTicketValidator, InitializingBean {
private String servicePrincipal;
private @Nullable String servicePrincipal;
private String realmName;
private @Nullable String realmName;
private Resource keyTabLocation;
private @Nullable Resource keyTabLocation;
private Subject serviceSubject;
private @Nullable Subject serviceSubject;
private boolean holdOnToGSSContext;
@ -79,6 +80,9 @@ public class SunJaasKerberosTicketValidator implements KerberosTicketValidator,
@Override
public KerberosTicketValidation validateTicket(byte[] token) {
try {
if (this.serviceSubject == null) {
throw new IllegalStateException("serviceSubject must be initialized");
}
if (!this.multiTier) {
return Subject.doAs(this.serviceSubject, new KerberosValidateAction(token));
}
@ -89,7 +93,7 @@ public class SunJaasKerberosTicketValidator implements KerberosTicketValidator,
return Subject.doAs(subjectHolder.getJaasSubject(), new KerberosMultitierValidateAction(token));
}
catch (PrivilegedActionException ex) {
catch (IllegalStateException | PrivilegedActionException ex) {
throw new BadCredentialsException("Kerberos validation not successful", ex);
}
}
@ -98,6 +102,9 @@ public class SunJaasKerberosTicketValidator implements KerberosTicketValidator,
public void afterPropertiesSet() throws Exception {
Assert.notNull(this.servicePrincipal, "servicePrincipal must be specified");
Assert.notNull(this.keyTabLocation, "keyTab must be specified");
if (this.servicePrincipal == null || this.keyTabLocation == null) {
throw new IllegalStateException("servicePrincipal and keyTabLocation must be set");
}
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.");
@ -263,8 +270,15 @@ public class SunJaasKerberosTicketValidator implements KerberosTicketValidator,
if (!SunJaasKerberosTicketValidator.this.holdOnToGSSContext) {
context.dispose();
}
return new KerberosTicketValidation(gssName.toString(),
SunJaasKerberosTicketValidator.this.servicePrincipal, responseToken, context, delegationCredential);
if (gssName == null) {
throw new BadCredentialsException("GSSContext name of the context initiator is null");
}
String servicePrincipal = SunJaasKerberosTicketValidator.this.servicePrincipal;
if (servicePrincipal == null) {
throw new IllegalStateException("servicePrincipal must be set");
}
return new KerberosTicketValidation(gssName.toString(), servicePrincipal, responseToken, context,
delegationCredential);
}
}
@ -280,7 +294,7 @@ public class SunJaasKerberosTicketValidator implements KerberosTicketValidator,
private String servicePrincipalName;
private String realmName;
private @Nullable String realmName;
private boolean multiTier;
@ -288,8 +302,8 @@ public class SunJaasKerberosTicketValidator implements KerberosTicketValidator,
private boolean refreshKrb5Config;
private LoginConfig(String keyTabLocation, String servicePrincipalName, String realmName, boolean multiTier,
boolean debug, boolean refreshKrb5Config) {
private LoginConfig(String keyTabLocation, String servicePrincipalName, @Nullable String realmName,
boolean multiTier, boolean debug, boolean refreshKrb5Config) {
this.keyTabLocation = keyTabLocation;
this.servicePrincipalName = servicePrincipalName;
this.realmName = realmName;

View File

@ -0,0 +1,20 @@
/*
* 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.
*/
@NullMarked
package org.springframework.security.kerberos.authentication.sun;
import org.jspecify.annotations.NullMarked;