From 7c887d2da1b201f15815e180365f7cf7955e9fb5 Mon Sep 17 00:00:00 2001 From: Rob Winch <362503+rwinch@users.noreply.github.com> Date: Wed, 9 Jul 2025 08:19:59 -0500 Subject: [PATCH] Add nullability to spring-security-core Closes gh-17534 --- .../web/OAuth2ResourceServerDslTests.kt | 2 +- .../web/session/SessionConcurrencyDslTests.kt | 2 +- .../web/server/ServerFormLoginDslTests.kt | 2 +- .../web/server/ServerHttpBasicDslTests.kt | 2 +- .../web/server/ServerHttpSecurityDslTests.kt | 2 +- .../web/server/ServerOAuth2ClientDslTests.kt | 2 +- core/spring-security-core.gradle | 4 + .../Jsr250MethodSecurityMetadataSource.java | 7 +- ...curedAnnotationSecurityMetadataSource.java | 10 +- .../access/annotation/package-info.java | 3 + .../security/access/event/package-info.java | 3 + .../AbstractSecurityExpressionHandler.java | 14 +- .../access/expression/ExpressionUtils.java | 7 +- .../expression/SecurityExpressionRoot.java | 18 ++- ...tExpressionBasedMethodConfigAttribute.java | 10 +- ...efaultMethodSecurityExpressionHandler.java | 23 ++- ...essionBasedAnnotationAttributeFactory.java | 10 +- .../ExpressionBasedPreInvocationAdvice.java | 2 + .../MethodSecurityEvaluationContext.java | 6 +- .../MethodSecurityExpressionHandler.java | 5 +- .../MethodSecurityExpressionOperations.java | 10 +- .../method/MethodSecurityExpressionRoot.java | 18 ++- .../PostInvocationExpressionAttribute.java | 4 +- .../PreInvocationExpressionAttribute.java | 6 +- .../expression/method/package-info.java | 3 + .../access/expression/package-info.java | 3 + .../hierarchicalroles/RoleHierarchyImpl.java | 2 +- .../hierarchicalroles/package-info.java | 3 + .../AbstractSecurityInterceptor.java | 13 +- .../AfterInvocationProviderManager.java | 6 +- .../MethodInvocationPrivilegeEvaluator.java | 6 +- .../access/intercept/NullRunAsManager.java | 7 +- .../RunAsImplAuthenticationProvider.java | 7 +- .../access/intercept/RunAsManagerImpl.java | 9 +- .../MethodSecurityInterceptor.java | 5 +- .../MethodSecurityMetadataSourceAdvisor.java | 7 +- .../intercept/aopalliance/package-info.java | 3 + .../aspectj/MethodInvocationAdapter.java | 2 + .../intercept/aspectj/package-info.java | 3 + .../access/intercept/package-info.java | 3 + ...tFallbackMethodSecurityMetadataSource.java | 6 +- .../AbstractMethodSecurityMetadataSource.java | 2 + ...elegatingMethodSecurityMetadataSource.java | 8 +- .../MapBasedMethodSecurityMetadataSource.java | 13 +- .../method/MethodSecurityMetadataSource.java | 4 +- .../security/access/method/package-info.java | 3 + .../security/access/package-info.java | 3 + .../prepost/PostInvocationAdviceProvider.java | 5 +- ...PreInvocationAuthorizationAdviceVoter.java | 5 +- ...rePostAdviceReactiveMethodInterceptor.java | 11 +- ...ePostAnnotationSecurityMetadataSource.java | 9 +- .../PrePostInvocationAttributeFactory.java | 9 +- .../security/access/prepost/package-info.java | 3 + .../access/vote/AbstractAclVoter.java | 6 +- .../access/vote/RoleHierarchyVoter.java | 7 +- .../security/access/vote/package-info.java | 3 + .../aot/hint/CoreSecurityRuntimeHints.java | 4 +- .../aot/hint/OneTimeTokenRuntimeHints.java | 4 +- .../security/aot/hint/package-info.java | 20 +++ .../AbstractAuthenticationToken.java | 12 +- ...rDetailsReactiveAuthenticationManager.java | 18 ++- .../AnonymousAuthenticationProvider.java | 4 +- .../AuthenticationObservationContext.java | 13 +- .../AuthenticationObservationConvention.java | 9 +- .../AuthenticationProvider.java | 4 +- .../AuthenticationServiceException.java | 4 +- .../DefaultAuthenticationEventPublisher.java | 10 +- ...nternalAuthenticationServiceException.java | 4 +- .../ObservationAuthenticationManager.java | 1 + .../authentication/ProviderManager.java | 5 +- .../RememberMeAuthenticationProvider.java | 4 +- .../UsernamePasswordAuthenticationToken.java | 14 +- .../dao/DaoAuthenticationProvider.java | 20 ++- .../authentication/dao/package-info.java | 3 + .../authentication/event/package-info.java | 3 + .../AbstractJaasAuthenticationProvider.java | 13 +- .../DefaultJaasAuthenticationProvider.java | 1 + .../jaas/JaasAuthenticationProvider.java | 1 + .../jaas/JaasAuthenticationToken.java | 6 +- .../jaas/JaasNameCallbackHandler.java | 2 + .../jaas/JaasPasswordCallbackHandler.java | 5 +- .../jaas/SecurityContextLoginModule.java | 11 +- .../jaas/event/package-info.java | 3 + .../jaas/memory/InMemoryConfiguration.java | 8 +- .../jaas/memory/package-info.java | 3 + .../authentication/jaas/package-info.java | 3 + .../ott/InMemoryOneTimeTokenService.java | 6 +- .../ott/JdbcOneTimeTokenService.java | 11 +- .../ott/OneTimeTokenAuthenticationToken.java | 14 +- .../ott/OneTimeTokenService.java | 7 +- .../authentication/ott/package-info.java | 20 +++ .../ott/reactive/package-info.java | 20 +++ .../security/authentication/package-info.java | 3 + .../password/CompromisedPasswordChecker.java | 9 +- .../ReactiveCompromisedPasswordChecker.java | 7 +- .../authentication/password/package-info.java | 20 +++ .../authorization/AuthorizationManager.java | 8 +- .../AuthorizationObservationContext.java | 13 +- .../AuthorizationProxyFactory.java | 4 +- .../ObservationAuthorizationManager.java | 8 +- ...servationReactiveAuthorizationManager.java | 6 +- .../ReactiveAuthorizationManager.java | 3 +- .../authorization/event/package-info.java | 20 +++ .../AbstractAuthorizationManagerRegistry.java | 5 +- .../AbstractExpressionAttributeRegistry.java | 11 +- .../AuthorizationAdvisorProxyFactory.java | 21 ++- ...rizationManagerAfterMethodInterceptor.java | 11 +- ...ManagerAfterReactiveMethodInterceptor.java | 6 +- ...izationManagerBeforeMethodInterceptor.java | 15 +- ...anagerBeforeReactiveMethodInterceptor.java | 3 +- .../method/AuthorizationMethodPointcuts.java | 5 + ...uthorizeReturnObjectMethodInterceptor.java | 6 +- .../method/ExpressionAttribute.java | 5 - .../authorization/method/ExpressionUtils.java | 6 +- .../method/Jsr250AuthorizationManager.java | 22 +-- .../MethodAuthorizationDeniedHandler.java | 8 +- .../method/MethodInvocationResult.java | 7 +- ...rningMethodAuthorizationDeniedHandler.java | 6 +- .../PostAuthorizeAuthorizationManager.java | 10 +- ...tAuthorizeExpressionAttributeRegistry.java | 11 +- ...AuthorizeReactiveAuthorizationManager.java | 8 +- ...tFilterAuthorizationMethodInterceptor.java | 5 +- ...uthorizationReactiveMethodInterceptor.java | 15 +- ...PostFilterExpressionAttributeRegistry.java | 10 +- .../PreAuthorizeAuthorizationManager.java | 8 +- ...eAuthorizeExpressionAttributeRegistry.java | 12 +- ...AuthorizeReactiveAuthorizationManager.java | 6 +- ...eFilterAuthorizationMethodInterceptor.java | 5 +- ...uthorizationReactiveMethodInterceptor.java | 15 +- .../PreFilterExpressionAttributeRegistry.java | 12 +- .../method/ReactiveExpressionUtils.java | 3 +- .../method/ReactiveMethodInvocationUtils.java | 3 +- ...ctiveMethodAuthorizationDeniedHandler.java | 6 +- .../method/SecuredAuthorizationManager.java | 7 +- .../authorization/method/package-info.java | 20 +++ .../security/authorization/package-info.java | 20 +++ ...tractDelegatingSecurityContextSupport.java | 6 +- .../DelegatingSecurityContextCallable.java | 6 +- .../DelegatingSecurityContextExecutor.java | 4 +- ...egatingSecurityContextExecutorService.java | 6 +- .../DelegatingSecurityContextRunnable.java | 8 +- ...curityContextScheduledExecutorService.java | 4 +- .../security/concurrent/package-info.java | 20 +++ .../security/context/package-info.java | 20 +++ .../security/converter/RsaKeyConverters.java | 9 +- .../security/converter/package-info.java | 20 +++ .../security/core/Authentication.java | 8 +- .../core/AuthenticationException.java | 8 +- .../security/core/ComparableVersion.java | 17 +- .../core/SpringSecurityCoreVersion.java | 14 +- .../AbstractSecurityAnnotationScanner.java | 11 +- ...sionTemplateSecurityAnnotationScanner.java | 6 +- .../annotation/SecurityAnnotationScanner.java | 8 +- .../UniqueSecurityAnnotationScanner.java | 11 +- .../core/annotation/package-info.java | 20 +++ ...edAttributes2GrantedAuthoritiesMapper.java | 6 +- .../mapping/SimpleAuthorityMapper.java | 4 +- .../SimpleMappableAttributesRetriever.java | 2 +- .../core/authority/mapping/package-info.java | 3 + .../security/core/authority/package-info.java | 3 + .../GlobalSecurityContextHolderStrategy.java | 4 +- ...rvationSecurityContextChangedListener.java | 8 +- .../core/context/SecurityContext.java | 6 +- .../core/context/SecurityContextHolder.java | 2 +- ...urityContextHolderThreadLocalAccessor.java | 3 +- .../core/context/SecurityContextImpl.java | 8 +- .../security/core/context/package-info.java | 3 + .../security/core/package-info.java | 3 + .../AnnotationParameterNameDiscoverer.java | 12 +- .../core/parameters/package-info.java | 20 +++ .../core/session/SessionRegistry.java | 4 +- .../core/session/SessionRegistryImpl.java | 3 +- .../security/core/session/package-info.java | 3 + .../KeyBasedPersistenceTokenService.java | 7 +- .../core/token/SecureRandomFactoryBean.java | 4 +- .../security/core/token/TokenService.java | 4 +- .../security/core/token/package-info.java | 3 + .../MapReactiveUserDetailsService.java | 5 +- .../ReactiveUserDetailsPasswordService.java | 5 +- .../security/core/userdetails/User.java | 35 ++-- .../security/core/userdetails/UserCache.java | 4 +- .../core/userdetails/UserDetails.java | 7 +- .../UserDetailsByNameServiceWrapper.java | 3 +- .../UserDetailsPasswordService.java | 6 +- .../UsernameNotFoundException.java | 17 +- .../core/userdetails/cache/NullUserCache.java | 4 +- .../cache/SpringCacheBasedUserCache.java | 3 +- .../core/userdetails/cache/package-info.java | 4 + .../core/userdetails/jdbc/JdbcDaoImpl.java | 13 +- .../core/userdetails/jdbc/package-info.java | 3 + .../userdetails/memory/UserAttribute.java | 6 +- .../core/userdetails/memory/package-info.java | 3 + .../core/userdetails/package-info.java | 3 + .../jackson2/SecurityJackson2Modules.java | 3 +- ...sswordAuthenticationTokenDeserializer.java | 3 +- .../security/jackson2/package-info.java | 3 + .../InMemoryUserDetailsManager.java | 9 +- .../provisioning/JdbcUserDetailsManager.java | 23 ++- .../security/provisioning/MutableUser.java | 8 +- .../provisioning/MutableUserDetails.java | 4 +- .../security/provisioning/package-info.java | 3 + ...SecurityContextSchedulingTaskExecutor.java | 4 +- ...elegatingSecurityContextTaskScheduler.java | 8 +- .../security/scheduling/package-info.java | 20 +++ ...atingSecurityContextAsyncTaskExecutor.java | 4 +- ...DelegatingSecurityContextTaskExecutor.java | 5 +- .../security/task/package-info.java | 20 +++ .../security/util/FieldUtils.java | 4 +- .../security/util/InMemoryResource.java | 8 +- .../security/util/MethodInvocationUtils.java | 9 +- .../security/util/SimpleMethodInvocation.java | 15 +- .../security/util/package-info.java | 3 + .../expression/ExpressionUtilsTests.java | 37 +++++ .../MethodSecurityEvaluationContextTests.java | 2 +- .../dao/DaoAuthenticationProviderTests.java | 16 +- ...efaultJaasAuthenticationProviderTests.java | 9 +- ...ributes2GrantedAuthoritiesMapperTests.java | 6 - ...onSecurityContextChangedListenerTests.java | 19 +++ .../security/core/userdetails/UserTests.java | 44 +++++- .../InMemoryUserDetailsManagerTests.java | 12 ++ .../JdbcUserDetailsManagerTests.java | 9 ++ .../security/util/InMemoryResourceTests.java | 2 +- .../crypto/argon2/Argon2PasswordEncoder.java | 18 +-- .../crypto/bcrypt/BCryptPasswordEncoder.java | 24 +-- .../security/crypto/codec/Utf8.java | 7 +- .../password/AbstractPasswordEncoder.java | 14 +- .../AbstractValidatingPasswordEncoder.java | 56 +++++++ .../password/DelegatingPasswordEncoder.java | 23 +-- .../password/LdapShaPasswordEncoder.java | 18 +-- .../crypto/password/Md4PasswordEncoder.java | 10 +- .../MessageDigestPasswordEncoder.java | 10 +- .../crypto/password/NoOpPasswordEncoder.java | 6 +- .../crypto/password/PasswordEncoder.java | 32 ++-- .../password/Pbkdf2PasswordEncoder.java | 16 +- .../password/StandardPasswordEncoder.java | 10 +- .../crypto/scrypt/SCryptPasswordEncoder.java | 17 +- .../argon2/Argon2PasswordEncoderTests.java | 90 ++++++----- .../bcrypt/BCryptPasswordEncoderTests.java | 149 ++++++++---------- .../AbstractPasswordEncoderTests.java | 40 +++++ ...bstractPasswordEncoderValidationTests.java | 70 ++++++++ ...bstractValidatingPasswordEncoderTests.java | 43 +++++ .../DelegatingPasswordEncoderTests.java | 54 +++---- .../password/LdapShaPasswordEncoderTests.java | 69 ++++---- .../password/Md4PasswordEncoderTests.java | 43 +++-- .../MessageDigestPasswordEncoderTests.java | 45 +++--- .../password/NoOpPasswordEncoderTests.java | 33 ++++ .../HaveIBeenPwnedRestApiPasswordChecker.java | 5 +- ...enPwnedRestApiReactivePasswordChecker.java | 9 +- ...edRestApiReactivePasswordCheckerTests.java | 7 + 249 files changed, 1888 insertions(+), 839 deletions(-) create mode 100644 core/src/main/java/org/springframework/security/aot/hint/package-info.java create mode 100644 core/src/main/java/org/springframework/security/authentication/ott/package-info.java create mode 100644 core/src/main/java/org/springframework/security/authentication/ott/reactive/package-info.java create mode 100644 core/src/main/java/org/springframework/security/authentication/password/package-info.java create mode 100644 core/src/main/java/org/springframework/security/authorization/event/package-info.java create mode 100644 core/src/main/java/org/springframework/security/authorization/method/package-info.java create mode 100644 core/src/main/java/org/springframework/security/authorization/package-info.java create mode 100644 core/src/main/java/org/springframework/security/concurrent/package-info.java create mode 100644 core/src/main/java/org/springframework/security/context/package-info.java create mode 100644 core/src/main/java/org/springframework/security/converter/package-info.java create mode 100644 core/src/main/java/org/springframework/security/core/annotation/package-info.java create mode 100644 core/src/main/java/org/springframework/security/core/parameters/package-info.java create mode 100644 core/src/main/java/org/springframework/security/scheduling/package-info.java create mode 100644 core/src/main/java/org/springframework/security/task/package-info.java create mode 100644 core/src/test/java/org/springframework/security/access/expression/ExpressionUtilsTests.java create mode 100644 crypto/src/main/java/org/springframework/security/crypto/password/AbstractValidatingPasswordEncoder.java create mode 100644 crypto/src/test/java/org/springframework/security/crypto/password/AbstractPasswordEncoderTests.java create mode 100644 crypto/src/test/java/org/springframework/security/crypto/password/AbstractPasswordEncoderValidationTests.java create mode 100644 crypto/src/test/java/org/springframework/security/crypto/password/AbstractValidatingPasswordEncoderTests.java create mode 100644 crypto/src/test/java/org/springframework/security/crypto/password/NoOpPasswordEncoderTests.java diff --git a/config/src/test/kotlin/org/springframework/security/config/annotation/web/OAuth2ResourceServerDslTests.kt b/config/src/test/kotlin/org/springframework/security/config/annotation/web/OAuth2ResourceServerDslTests.kt index 95d0599cfb..07c89272fc 100644 --- a/config/src/test/kotlin/org/springframework/security/config/annotation/web/OAuth2ResourceServerDslTests.kt +++ b/config/src/test/kotlin/org/springframework/security/config/annotation/web/OAuth2ResourceServerDslTests.kt @@ -242,7 +242,7 @@ class OAuth2ResourceServerDslTests { class MockAuthenticationManager(var authentication: Authentication) : AuthenticationManager { - override fun authenticate(authentication: Authentication?): Authentication { + override fun authenticate(authentication: Authentication): Authentication { return this.authentication } diff --git a/config/src/test/kotlin/org/springframework/security/config/annotation/web/session/SessionConcurrencyDslTests.kt b/config/src/test/kotlin/org/springframework/security/config/annotation/web/session/SessionConcurrencyDslTests.kt index 9117ae757a..4ac1a05e7a 100644 --- a/config/src/test/kotlin/org/springframework/security/config/annotation/web/session/SessionConcurrencyDslTests.kt +++ b/config/src/test/kotlin/org/springframework/security/config/annotation/web/session/SessionConcurrencyDslTests.kt @@ -217,7 +217,7 @@ class SessionConcurrencyDslTests { sessionManagement { sessionConcurrency { maximumSessions { - authentication -> if (isAdmin.authorize({ authentication }, null)!!.isGranted) -1 else 1 + authentication -> if (isAdmin.authorize({ authentication }, "")!!.isGranted) -1 else 1 } maxSessionsPreventsLogin = true } diff --git a/config/src/test/kotlin/org/springframework/security/config/web/server/ServerFormLoginDslTests.kt b/config/src/test/kotlin/org/springframework/security/config/web/server/ServerFormLoginDslTests.kt index 6a547a83fe..fbe2bb30e2 100644 --- a/config/src/test/kotlin/org/springframework/security/config/web/server/ServerFormLoginDslTests.kt +++ b/config/src/test/kotlin/org/springframework/security/config/web/server/ServerFormLoginDslTests.kt @@ -172,7 +172,7 @@ class ServerFormLoginDslTests { } class NoopReactiveAuthenticationManager: ReactiveAuthenticationManager { - override fun authenticate(authentication: Authentication?): Mono { + override fun authenticate(authentication: Authentication): Mono { return Mono.empty() } } diff --git a/config/src/test/kotlin/org/springframework/security/config/web/server/ServerHttpBasicDslTests.kt b/config/src/test/kotlin/org/springframework/security/config/web/server/ServerHttpBasicDslTests.kt index c978ccb1b1..6647fb9e3c 100644 --- a/config/src/test/kotlin/org/springframework/security/config/web/server/ServerHttpBasicDslTests.kt +++ b/config/src/test/kotlin/org/springframework/security/config/web/server/ServerHttpBasicDslTests.kt @@ -151,7 +151,7 @@ class ServerHttpBasicDslTests { } class NoopReactiveAuthenticationManager: ReactiveAuthenticationManager { - override fun authenticate(authentication: Authentication?): Mono { + override fun authenticate(authentication: Authentication): Mono { return Mono.empty() } } diff --git a/config/src/test/kotlin/org/springframework/security/config/web/server/ServerHttpSecurityDslTests.kt b/config/src/test/kotlin/org/springframework/security/config/web/server/ServerHttpSecurityDslTests.kt index c0952780c0..96477c1d1a 100644 --- a/config/src/test/kotlin/org/springframework/security/config/web/server/ServerHttpSecurityDslTests.kt +++ b/config/src/test/kotlin/org/springframework/security/config/web/server/ServerHttpSecurityDslTests.kt @@ -249,7 +249,7 @@ class ServerHttpSecurityDslTests { } class NoopReactiveAuthenticationManager: ReactiveAuthenticationManager { - override fun authenticate(authentication: Authentication?): Mono { + override fun authenticate(authentication: Authentication): Mono { return Mono.empty() } } diff --git a/config/src/test/kotlin/org/springframework/security/config/web/server/ServerOAuth2ClientDslTests.kt b/config/src/test/kotlin/org/springframework/security/config/web/server/ServerOAuth2ClientDslTests.kt index cbdb8d738c..a5c3e4261e 100644 --- a/config/src/test/kotlin/org/springframework/security/config/web/server/ServerOAuth2ClientDslTests.kt +++ b/config/src/test/kotlin/org/springframework/security/config/web/server/ServerOAuth2ClientDslTests.kt @@ -276,7 +276,7 @@ class ServerOAuth2ClientDslTests { } class NoopReactiveAuthenticationManager: ReactiveAuthenticationManager { - override fun authenticate(authentication: Authentication?): Mono { + override fun authenticate(authentication: Authentication): Mono { return Mono.empty() } } diff --git a/core/spring-security-core.gradle b/core/spring-security-core.gradle index a81cc81d60..2292388d9f 100644 --- a/core/spring-security-core.gradle +++ b/core/spring-security-core.gradle @@ -1,5 +1,9 @@ import java.util.concurrent.Callable +plugins { + id 'security-nullability' +} + apply plugin: 'io.spring.convention.spring-module' apply plugin: 'security-kotlin' diff --git a/core/src/main/java/org/springframework/security/access/annotation/Jsr250MethodSecurityMetadataSource.java b/core/src/main/java/org/springframework/security/access/annotation/Jsr250MethodSecurityMetadataSource.java index 27992654d4..fc19138609 100644 --- a/core/src/main/java/org/springframework/security/access/annotation/Jsr250MethodSecurityMetadataSource.java +++ b/core/src/main/java/org/springframework/security/access/annotation/Jsr250MethodSecurityMetadataSource.java @@ -25,6 +25,8 @@ import java.util.List; import jakarta.annotation.security.DenyAll; import jakarta.annotation.security.PermitAll; import jakarta.annotation.security.RolesAllowed; +import org.jspecify.annotations.NullUnmarked; +import org.jspecify.annotations.Nullable; import org.springframework.core.annotation.AnnotationUtils; import org.springframework.security.access.ConfigAttribute; @@ -39,6 +41,7 @@ import org.springframework.security.access.method.AbstractFallbackMethodSecurity * {@link org.springframework.security.authorization.method.Jsr250AuthorizationManager} * instead */ +@NullUnmarked @Deprecated public class Jsr250MethodSecurityMetadataSource extends AbstractFallbackMethodSecurityMetadataSource { @@ -71,11 +74,11 @@ public class Jsr250MethodSecurityMetadataSource extends AbstractFallbackMethodSe } @Override - public Collection getAllConfigAttributes() { + public @Nullable Collection getAllConfigAttributes() { return null; } - private List processAnnotations(Annotation[] annotations) { + private @Nullable List processAnnotations(Annotation @Nullable [] annotations) { if (annotations == null || annotations.length == 0) { return null; } diff --git a/core/src/main/java/org/springframework/security/access/annotation/SecuredAnnotationSecurityMetadataSource.java b/core/src/main/java/org/springframework/security/access/annotation/SecuredAnnotationSecurityMetadataSource.java index 1ff5c28ed3..47f33288fd 100644 --- a/core/src/main/java/org/springframework/security/access/annotation/SecuredAnnotationSecurityMetadataSource.java +++ b/core/src/main/java/org/springframework/security/access/annotation/SecuredAnnotationSecurityMetadataSource.java @@ -22,6 +22,9 @@ import java.util.ArrayList; import java.util.Collection; import java.util.List; +import org.jspecify.annotations.NullUnmarked; +import org.jspecify.annotations.Nullable; + import org.springframework.core.GenericTypeResolver; import org.springframework.core.annotation.AnnotationUtils; import org.springframework.security.access.ConfigAttribute; @@ -41,13 +44,14 @@ import org.springframework.util.Assert; * @deprecated Use * {@link org.springframework.security.authorization.method.AuthorizationManagerBeforeMethodInterceptor#secured} */ +@NullUnmarked @Deprecated @SuppressWarnings({ "unchecked" }) public class SecuredAnnotationSecurityMetadataSource extends AbstractFallbackMethodSecurityMetadataSource { private AnnotationMetadataExtractor annotationExtractor; - private Class annotationType; + private @Nullable Class annotationType; public SecuredAnnotationSecurityMetadataSource() { this(new SecuredAnnotationMetadataExtractor()); @@ -73,11 +77,11 @@ public class SecuredAnnotationSecurityMetadataSource extends AbstractFallbackMet } @Override - public Collection getAllConfigAttributes() { + public @Nullable Collection getAllConfigAttributes() { return null; } - private Collection processAnnotation(Annotation annotation) { + private @Nullable Collection processAnnotation(@Nullable Annotation annotation) { return (annotation != null) ? this.annotationExtractor.extractAttributes(annotation) : null; } diff --git a/core/src/main/java/org/springframework/security/access/annotation/package-info.java b/core/src/main/java/org/springframework/security/access/annotation/package-info.java index 269a726520..28d3c1f35b 100644 --- a/core/src/main/java/org/springframework/security/access/annotation/package-info.java +++ b/core/src/main/java/org/springframework/security/access/annotation/package-info.java @@ -17,4 +17,7 @@ /** * Support for JSR-250 and Spring Security {@code @Secured} annotations. */ +@NullMarked package org.springframework.security.access.annotation; + +import org.jspecify.annotations.NullMarked; diff --git a/core/src/main/java/org/springframework/security/access/event/package-info.java b/core/src/main/java/org/springframework/security/access/event/package-info.java index 41488584ec..ea06fb42cd 100644 --- a/core/src/main/java/org/springframework/security/access/event/package-info.java +++ b/core/src/main/java/org/springframework/security/access/event/package-info.java @@ -17,4 +17,7 @@ /** * Authorization event and listener classes. */ +@NullMarked package org.springframework.security.access.event; + +import org.jspecify.annotations.NullMarked; diff --git a/core/src/main/java/org/springframework/security/access/expression/AbstractSecurityExpressionHandler.java b/core/src/main/java/org/springframework/security/access/expression/AbstractSecurityExpressionHandler.java index 8da167f306..fc7e088859 100644 --- a/core/src/main/java/org/springframework/security/access/expression/AbstractSecurityExpressionHandler.java +++ b/core/src/main/java/org/springframework/security/access/expression/AbstractSecurityExpressionHandler.java @@ -16,6 +16,8 @@ package org.springframework.security.access.expression; +import org.jspecify.annotations.Nullable; + import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; import org.springframework.context.expression.BeanFactoryResolver; @@ -43,9 +45,9 @@ public abstract class AbstractSecurityExpressionHandler private ExpressionParser expressionParser = new SpelExpressionParser(); - private BeanResolver beanResolver; + private @Nullable BeanResolver beanResolver; - private RoleHierarchy roleHierarchy; + private @Nullable RoleHierarchy roleHierarchy; private PermissionEvaluator permissionEvaluator = new DenyAllPermissionEvaluator(); @@ -71,7 +73,9 @@ public abstract class AbstractSecurityExpressionHandler public final EvaluationContext createEvaluationContext(Authentication authentication, T invocation) { SecurityExpressionOperations root = createSecurityExpressionRoot(authentication, invocation); StandardEvaluationContext ctx = createEvaluationContextInternal(authentication, invocation); - ctx.setBeanResolver(this.beanResolver); + if (this.beanResolver != null) { + ctx.setBeanResolver(this.beanResolver); + } ctx.setRootObject(root); return ctx; } @@ -101,7 +105,7 @@ public abstract class AbstractSecurityExpressionHandler protected abstract SecurityExpressionOperations createSecurityExpressionRoot(Authentication authentication, T invocation); - protected RoleHierarchy getRoleHierarchy() { + protected @Nullable RoleHierarchy getRoleHierarchy() { return this.roleHierarchy; } @@ -117,7 +121,7 @@ public abstract class AbstractSecurityExpressionHandler this.permissionEvaluator = permissionEvaluator; } - protected BeanResolver getBeanResolver() { + protected @Nullable BeanResolver getBeanResolver() { return this.beanResolver; } diff --git a/core/src/main/java/org/springframework/security/access/expression/ExpressionUtils.java b/core/src/main/java/org/springframework/security/access/expression/ExpressionUtils.java index 44d60a6739..9677c6ca50 100644 --- a/core/src/main/java/org/springframework/security/access/expression/ExpressionUtils.java +++ b/core/src/main/java/org/springframework/security/access/expression/ExpressionUtils.java @@ -27,7 +27,12 @@ public final class ExpressionUtils { public static boolean evaluateAsBoolean(Expression expr, EvaluationContext ctx) { try { - return expr.getValue(ctx, Boolean.class); + Boolean result = expr.getValue(ctx, Boolean.class); + if (result == null) { + throw new IllegalArgumentException( + "Expression was null but expected boolean result '" + expr.getExpressionString() + "'"); + } + return result; } catch (EvaluationException ex) { throw new IllegalArgumentException("Failed to evaluate expression '" + expr.getExpressionString() + "'", diff --git a/core/src/main/java/org/springframework/security/access/expression/SecurityExpressionRoot.java b/core/src/main/java/org/springframework/security/access/expression/SecurityExpressionRoot.java index df4eac2877..857ec3abe2 100644 --- a/core/src/main/java/org/springframework/security/access/expression/SecurityExpressionRoot.java +++ b/core/src/main/java/org/springframework/security/access/expression/SecurityExpressionRoot.java @@ -21,9 +21,12 @@ import java.util.Collection; import java.util.Set; import java.util.function.Supplier; +import org.jspecify.annotations.Nullable; + import org.springframework.security.access.PermissionEvaluator; import org.springframework.security.access.hierarchicalroles.RoleHierarchy; import org.springframework.security.authentication.AuthenticationTrustResolver; +import org.springframework.security.authentication.AuthenticationTrustResolverImpl; import org.springframework.security.core.Authentication; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.AuthorityUtils; @@ -41,11 +44,11 @@ public abstract class SecurityExpressionRoot implements SecurityExpressionOperat private final Supplier authentication; - private AuthenticationTrustResolver trustResolver; + private AuthenticationTrustResolver trustResolver = new AuthenticationTrustResolverImpl(); - private RoleHierarchy roleHierarchy; + private @Nullable RoleHierarchy roleHierarchy; - private Set roles; + private @Nullable Set roles; private String defaultRolePrefix = "ROLE_"; @@ -59,7 +62,7 @@ public abstract class SecurityExpressionRoot implements SecurityExpressionOperat */ public final boolean denyAll = false; - private PermissionEvaluator permissionEvaluator; + private PermissionEvaluator permissionEvaluator = new DenyAllPermissionEvaluator(); public final String read = "read"; @@ -114,7 +117,7 @@ public abstract class SecurityExpressionRoot implements SecurityExpressionOperat return hasAnyAuthorityName(this.defaultRolePrefix, roles); } - private boolean hasAnyAuthorityName(String prefix, String... roles) { + private boolean hasAnyAuthorityName(@Nullable String prefix, String... roles) { Set roleSet = getAuthoritySet(); for (String role : roles) { String defaultedRole = getRoleWithDefaultPrefix(prefix, role); @@ -166,7 +169,7 @@ public abstract class SecurityExpressionRoot implements SecurityExpressionOperat * {@link #getAuthentication()} * @return */ - public Object getPrincipal() { + public @Nullable Object getPrincipal() { return getAuthentication().getPrincipal(); } @@ -218,6 +221,7 @@ public abstract class SecurityExpressionRoot implements SecurityExpressionOperat } public void setPermissionEvaluator(PermissionEvaluator permissionEvaluator) { + Assert.notNull(permissionEvaluator, "permissionEvaluator cannot be null"); this.permissionEvaluator = permissionEvaluator; } @@ -228,7 +232,7 @@ public abstract class SecurityExpressionRoot implements SecurityExpressionOperat * @param role * @return */ - private static String getRoleWithDefaultPrefix(String defaultRolePrefix, String role) { + private static String getRoleWithDefaultPrefix(@Nullable String defaultRolePrefix, String role) { if (role == null) { return role; } diff --git a/core/src/main/java/org/springframework/security/access/expression/method/AbstractExpressionBasedMethodConfigAttribute.java b/core/src/main/java/org/springframework/security/access/expression/method/AbstractExpressionBasedMethodConfigAttribute.java index 95a2725a6b..b23341c573 100644 --- a/core/src/main/java/org/springframework/security/access/expression/method/AbstractExpressionBasedMethodConfigAttribute.java +++ b/core/src/main/java/org/springframework/security/access/expression/method/AbstractExpressionBasedMethodConfigAttribute.java @@ -16,6 +16,9 @@ package org.springframework.security.access.expression.method; +import org.jspecify.annotations.NullUnmarked; +import org.jspecify.annotations.Nullable; + import org.springframework.expression.Expression; import org.springframework.expression.ParseException; import org.springframework.expression.spel.standard.SpelExpressionParser; @@ -35,12 +38,13 @@ import org.springframework.util.Assert; * @deprecated Use {@link org.springframework.security.authorization.AuthorizationManager} * interceptors instead */ +@NullUnmarked @Deprecated abstract class AbstractExpressionBasedMethodConfigAttribute implements ConfigAttribute { - private final Expression filterExpression; + private final @Nullable Expression filterExpression; - private final Expression authorizeExpression; + private final @Nullable Expression authorizeExpression; /** * Parses the supplied expressions as Spring-EL. @@ -71,7 +75,7 @@ abstract class AbstractExpressionBasedMethodConfigAttribute implements ConfigAtt } @Override - public String getAttribute() { + public @Nullable String getAttribute() { return null; } diff --git a/core/src/main/java/org/springframework/security/access/expression/method/DefaultMethodSecurityExpressionHandler.java b/core/src/main/java/org/springframework/security/access/expression/method/DefaultMethodSecurityExpressionHandler.java index 2460bb6056..c14feb5e15 100644 --- a/core/src/main/java/org/springframework/security/access/expression/method/DefaultMethodSecurityExpressionHandler.java +++ b/core/src/main/java/org/springframework/security/access/expression/method/DefaultMethodSecurityExpressionHandler.java @@ -23,17 +23,20 @@ import java.util.Collection; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.function.Supplier; import java.util.stream.Stream; import org.aopalliance.intercept.MethodInvocation; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.Nullable; import org.springframework.core.ParameterNameDiscoverer; import org.springframework.core.log.LogMessage; import org.springframework.expression.EvaluationContext; import org.springframework.expression.Expression; +import org.springframework.expression.TypedValue; import org.springframework.expression.spel.support.StandardEvaluationContext; import org.springframework.security.access.PermissionCacheOptimizer; import org.springframework.security.access.expression.AbstractSecurityExpressionHandler; @@ -64,7 +67,7 @@ public class DefaultMethodSecurityExpressionHandler extends AbstractSecurityExpr private ParameterNameDiscoverer parameterNameDiscoverer = new DefaultSecurityParameterNameDiscoverer(); - private PermissionCacheOptimizer permissionCacheOptimizer = null; + private @Nullable PermissionCacheOptimizer permissionCacheOptimizer = null; private String defaultRolePrefix = "ROLE_"; @@ -85,7 +88,7 @@ public class DefaultMethodSecurityExpressionHandler extends AbstractSecurityExpr MethodSecurityExpressionOperations root = createSecurityExpressionRoot(authentication, mi); MethodSecurityEvaluationContext ctx = new MethodSecurityEvaluationContext(root, mi, getParameterNameDiscoverer()); - ctx.setBeanResolver(getBeanResolver()); + Optional.ofNullable(getBeanResolver()).ifPresent(ctx::setBeanResolver); return ctx; } @@ -104,7 +107,7 @@ public class DefaultMethodSecurityExpressionHandler extends AbstractSecurityExpr root.setThis(invocation.getThis()); root.setPermissionEvaluator(getPermissionEvaluator()); root.setTrustResolver(getTrustResolver()); - root.setRoleHierarchy(getRoleHierarchy()); + Optional.ofNullable(getRoleHierarchy()).ifPresent(root::setRoleHierarchy); root.setDefaultRolePrefix(getDefaultRolePrefix()); return root; } @@ -119,14 +122,15 @@ public class DefaultMethodSecurityExpressionHandler extends AbstractSecurityExpr * {@link Stream} */ @Override - public Object filter(Object filterTarget, Expression filterExpression, EvaluationContext ctx) { + public Object filter(@Nullable Object filterTarget, Expression filterExpression, EvaluationContext ctx) { MethodSecurityExpressionOperations rootObject = (MethodSecurityExpressionOperations) ctx.getRootObject() .getValue(); + Assert.notNull(rootObject, "rootObject cannot be null"); this.logger.debug(LogMessage.format("Filtering with expression: %s", filterExpression.getExpressionString())); if (filterTarget instanceof Collection) { return filterCollection((Collection) filterTarget, filterExpression, ctx, rootObject); } - if (filterTarget.getClass().isArray()) { + if (filterTarget != null && filterTarget.getClass().isArray()) { return filterArray((Object[]) filterTarget, filterExpression, ctx, rootObject); } if (filterTarget instanceof Map) { @@ -259,8 +263,13 @@ public class DefaultMethodSecurityExpressionHandler extends AbstractSecurityExpr } @Override - public void setReturnObject(Object returnObject, EvaluationContext ctx) { - ((MethodSecurityExpressionOperations) ctx.getRootObject().getValue()).setReturnObject(returnObject); + public void setReturnObject(@Nullable Object returnObject, EvaluationContext ctx) { + TypedValue rootObject = ctx.getRootObject(); + Assert.notNull(rootObject, "rootObject cannot be null"); + MethodSecurityExpressionOperations methodOperations = (MethodSecurityExpressionOperations) rootObject + .getValue(); + Assert.notNull(methodOperations, "MethodSecurityExpressionOperations cannot be null"); + methodOperations.setReturnObject(returnObject); } /** diff --git a/core/src/main/java/org/springframework/security/access/expression/method/ExpressionBasedAnnotationAttributeFactory.java b/core/src/main/java/org/springframework/security/access/expression/method/ExpressionBasedAnnotationAttributeFactory.java index b309e890a8..0349a7f231 100644 --- a/core/src/main/java/org/springframework/security/access/expression/method/ExpressionBasedAnnotationAttributeFactory.java +++ b/core/src/main/java/org/springframework/security/access/expression/method/ExpressionBasedAnnotationAttributeFactory.java @@ -16,12 +16,16 @@ package org.springframework.security.access.expression.method; +import org.jspecify.annotations.NullUnmarked; +import org.jspecify.annotations.Nullable; + import org.springframework.expression.Expression; import org.springframework.expression.ExpressionParser; import org.springframework.expression.ParseException; import org.springframework.security.access.prepost.PostInvocationAttribute; import org.springframework.security.access.prepost.PreInvocationAttribute; import org.springframework.security.access.prepost.PrePostInvocationAttributeFactory; +import org.springframework.util.Assert; /** * {@link PrePostInvocationAttributeFactory} which interprets the annotation value as an @@ -33,16 +37,18 @@ import org.springframework.security.access.prepost.PrePostInvocationAttributeFac * @deprecated Use {@link org.springframework.security.authorization.AuthorizationManager} * interceptors instead */ +@NullUnmarked @Deprecated public class ExpressionBasedAnnotationAttributeFactory implements PrePostInvocationAttributeFactory { private final Object parserLock = new Object(); - private ExpressionParser parser; + private @Nullable ExpressionParser parser; private MethodSecurityExpressionHandler handler; public ExpressionBasedAnnotationAttributeFactory(MethodSecurityExpressionHandler handler) { + Assert.notNull(handler, "handler cannot be null"); this.handler = handler; } @@ -64,7 +70,7 @@ public class ExpressionBasedAnnotationAttributeFactory implements PrePostInvocat } @Override - public PostInvocationAttribute createPostInvocationAttribute(String postFilterAttribute, + public @Nullable PostInvocationAttribute createPostInvocationAttribute(String postFilterAttribute, String postAuthorizeAttribute) { try { ExpressionParser parser = getParser(); diff --git a/core/src/main/java/org/springframework/security/access/expression/method/ExpressionBasedPreInvocationAdvice.java b/core/src/main/java/org/springframework/security/access/expression/method/ExpressionBasedPreInvocationAdvice.java index 82f295ab23..b867c9b80d 100644 --- a/core/src/main/java/org/springframework/security/access/expression/method/ExpressionBasedPreInvocationAdvice.java +++ b/core/src/main/java/org/springframework/security/access/expression/method/ExpressionBasedPreInvocationAdvice.java @@ -19,6 +19,7 @@ package org.springframework.security.access.expression.method; import java.util.Collection; import org.aopalliance.intercept.MethodInvocation; +import org.jspecify.annotations.NullUnmarked; import org.springframework.expression.EvaluationContext; import org.springframework.expression.Expression; @@ -37,6 +38,7 @@ import org.springframework.util.Assert; * {@link org.springframework.security.authorization.method.AuthorizationManagerAfterMethodInterceptor} * instead */ +@NullUnmarked @Deprecated public class ExpressionBasedPreInvocationAdvice implements PreInvocationAuthorizationAdvice { diff --git a/core/src/main/java/org/springframework/security/access/expression/method/MethodSecurityEvaluationContext.java b/core/src/main/java/org/springframework/security/access/expression/method/MethodSecurityEvaluationContext.java index b341356551..54fa7f2873 100644 --- a/core/src/main/java/org/springframework/security/access/expression/method/MethodSecurityEvaluationContext.java +++ b/core/src/main/java/org/springframework/security/access/expression/method/MethodSecurityEvaluationContext.java @@ -19,6 +19,7 @@ package org.springframework.security.access.expression.method; import java.lang.reflect.Method; import org.aopalliance.intercept.MethodInvocation; +import org.jspecify.annotations.NullUnmarked; import org.springframework.aop.framework.AopProxyUtils; import org.springframework.aop.support.AopUtils; @@ -48,6 +49,8 @@ class MethodSecurityEvaluationContext extends MethodBasedEvaluationContext { this(user, mi, new DefaultSecurityParameterNameDiscoverer()); } + @NullUnmarked // FIXME: rootObject in MethodBasedEvaluationContext is non-null + // (probably needs changed) but StandardEvaluationContext is Nullable MethodSecurityEvaluationContext(Authentication user, MethodInvocation mi, ParameterNameDiscoverer parameterNameDiscoverer) { super(mi.getThis(), getSpecificMethod(mi), mi.getArguments(), parameterNameDiscoverer); @@ -59,7 +62,8 @@ class MethodSecurityEvaluationContext extends MethodBasedEvaluationContext { } private static Method getSpecificMethod(MethodInvocation mi) { - return AopUtils.getMostSpecificMethod(mi.getMethod(), AopProxyUtils.ultimateTargetClass(mi.getThis())); + Class targetClass = (mi.getThis() != null) ? AopProxyUtils.ultimateTargetClass(mi.getThis()) : null; + return AopUtils.getMostSpecificMethod(mi.getMethod(), targetClass); } } diff --git a/core/src/main/java/org/springframework/security/access/expression/method/MethodSecurityExpressionHandler.java b/core/src/main/java/org/springframework/security/access/expression/method/MethodSecurityExpressionHandler.java index 50e0bfa76a..dfc30bfd87 100644 --- a/core/src/main/java/org/springframework/security/access/expression/method/MethodSecurityExpressionHandler.java +++ b/core/src/main/java/org/springframework/security/access/expression/method/MethodSecurityExpressionHandler.java @@ -17,6 +17,7 @@ package org.springframework.security.access.expression.method; import org.aopalliance.intercept.MethodInvocation; +import org.jspecify.annotations.Nullable; import org.springframework.expression.EvaluationContext; import org.springframework.expression.Expression; @@ -41,7 +42,7 @@ public interface MethodSecurityExpressionHandler extends SecurityExpressionHandl * {@link #createEvaluationContext(org.springframework.security.core.Authentication, Object)} * @return the filtered collection or array */ - Object filter(Object filterTarget, Expression filterExpression, EvaluationContext ctx); + Object filter(@Nullable Object filterTarget, Expression filterExpression, EvaluationContext ctx); /** * Used to inform the expression system of the return object for the given evaluation @@ -51,6 +52,6 @@ public interface MethodSecurityExpressionHandler extends SecurityExpressionHandl * call to * {@link #createEvaluationContext(org.springframework.security.core.Authentication, Object)} */ - void setReturnObject(Object returnObject, EvaluationContext ctx); + void setReturnObject(@Nullable Object returnObject, EvaluationContext ctx); } diff --git a/core/src/main/java/org/springframework/security/access/expression/method/MethodSecurityExpressionOperations.java b/core/src/main/java/org/springframework/security/access/expression/method/MethodSecurityExpressionOperations.java index d3553eba33..ecd7cbc19c 100644 --- a/core/src/main/java/org/springframework/security/access/expression/method/MethodSecurityExpressionOperations.java +++ b/core/src/main/java/org/springframework/security/access/expression/method/MethodSecurityExpressionOperations.java @@ -16,6 +16,8 @@ package org.springframework.security.access.expression.method; +import org.jspecify.annotations.Nullable; + import org.springframework.security.access.expression.SecurityExpressionOperations; /** @@ -29,12 +31,12 @@ public interface MethodSecurityExpressionOperations extends SecurityExpressionOp void setFilterObject(Object filterObject); - Object getFilterObject(); + @Nullable Object getFilterObject(); - void setReturnObject(Object returnObject); + void setReturnObject(@Nullable Object returnObject); - Object getReturnObject(); + @Nullable Object getReturnObject(); - Object getThis(); + @Nullable Object getThis(); } diff --git a/core/src/main/java/org/springframework/security/access/expression/method/MethodSecurityExpressionRoot.java b/core/src/main/java/org/springframework/security/access/expression/method/MethodSecurityExpressionRoot.java index 5e17069a53..9b88645b03 100644 --- a/core/src/main/java/org/springframework/security/access/expression/method/MethodSecurityExpressionRoot.java +++ b/core/src/main/java/org/springframework/security/access/expression/method/MethodSecurityExpressionRoot.java @@ -18,6 +18,8 @@ package org.springframework.security.access.expression.method; import java.util.function.Supplier; +import org.jspecify.annotations.Nullable; + import org.springframework.security.access.expression.SecurityExpressionRoot; import org.springframework.security.core.Authentication; @@ -30,11 +32,11 @@ import org.springframework.security.core.Authentication; */ class MethodSecurityExpressionRoot extends SecurityExpressionRoot implements MethodSecurityExpressionOperations { - private Object filterObject; + private @Nullable Object filterObject; - private Object returnObject; + private @Nullable Object returnObject; - private Object target; + private @Nullable Object target; MethodSecurityExpressionRoot(Authentication a) { super(a); @@ -50,17 +52,17 @@ class MethodSecurityExpressionRoot extends SecurityExpressionRoot implements Met } @Override - public Object getFilterObject() { + public @Nullable Object getFilterObject() { return this.filterObject; } @Override - public void setReturnObject(Object returnObject) { + public void setReturnObject(@Nullable Object returnObject) { this.returnObject = returnObject; } @Override - public Object getReturnObject() { + public @Nullable Object getReturnObject() { return this.returnObject; } @@ -70,12 +72,12 @@ class MethodSecurityExpressionRoot extends SecurityExpressionRoot implements Met * protected. * @param target the target object on which the method in is being invoked. */ - void setThis(Object target) { + void setThis(@Nullable Object target) { this.target = target; } @Override - public Object getThis() { + public @Nullable Object getThis() { return this.target; } diff --git a/core/src/main/java/org/springframework/security/access/expression/method/PostInvocationExpressionAttribute.java b/core/src/main/java/org/springframework/security/access/expression/method/PostInvocationExpressionAttribute.java index 8642484a41..c164d124b2 100644 --- a/core/src/main/java/org/springframework/security/access/expression/method/PostInvocationExpressionAttribute.java +++ b/core/src/main/java/org/springframework/security/access/expression/method/PostInvocationExpressionAttribute.java @@ -16,6 +16,8 @@ package org.springframework.security.access.expression.method; +import org.jspecify.annotations.Nullable; + import org.springframework.expression.Expression; import org.springframework.expression.ParseException; import org.springframework.security.access.prepost.PostInvocationAttribute; @@ -36,7 +38,7 @@ class PostInvocationExpressionAttribute extends AbstractExpressionBasedMethodCon super(filterExpression, authorizeExpression); } - PostInvocationExpressionAttribute(Expression filterExpression, Expression authorizeExpression) + PostInvocationExpressionAttribute(@Nullable Expression filterExpression, @Nullable Expression authorizeExpression) throws ParseException { super(filterExpression, authorizeExpression); } diff --git a/core/src/main/java/org/springframework/security/access/expression/method/PreInvocationExpressionAttribute.java b/core/src/main/java/org/springframework/security/access/expression/method/PreInvocationExpressionAttribute.java index 41ec280bc7..5cc0f1d5e5 100644 --- a/core/src/main/java/org/springframework/security/access/expression/method/PreInvocationExpressionAttribute.java +++ b/core/src/main/java/org/springframework/security/access/expression/method/PreInvocationExpressionAttribute.java @@ -16,6 +16,8 @@ package org.springframework.security.access.expression.method; +import org.jspecify.annotations.Nullable; + import org.springframework.expression.Expression; import org.springframework.expression.ParseException; import org.springframework.security.access.prepost.PreInvocationAttribute; @@ -40,8 +42,8 @@ class PreInvocationExpressionAttribute extends AbstractExpressionBasedMethodConf this.filterTarget = filterTarget; } - PreInvocationExpressionAttribute(Expression filterExpression, String filterTarget, Expression authorizeExpression) - throws ParseException { + PreInvocationExpressionAttribute(@Nullable Expression filterExpression, String filterTarget, + Expression authorizeExpression) throws ParseException { super(filterExpression, authorizeExpression); this.filterTarget = filterTarget; } diff --git a/core/src/main/java/org/springframework/security/access/expression/method/package-info.java b/core/src/main/java/org/springframework/security/access/expression/method/package-info.java index b559639982..3415739396 100644 --- a/core/src/main/java/org/springframework/security/access/expression/method/package-info.java +++ b/core/src/main/java/org/springframework/security/access/expression/method/package-info.java @@ -19,4 +19,7 @@ * * @since 3.0 */ +@NullMarked package org.springframework.security.access.expression.method; + +import org.jspecify.annotations.NullMarked; diff --git a/core/src/main/java/org/springframework/security/access/expression/package-info.java b/core/src/main/java/org/springframework/security/access/expression/package-info.java index b8873c1ddb..bb5af3dcae 100644 --- a/core/src/main/java/org/springframework/security/access/expression/package-info.java +++ b/core/src/main/java/org/springframework/security/access/expression/package-info.java @@ -22,4 +22,7 @@ * * @since 3.0 */ +@NullMarked package org.springframework.security.access.expression; + +import org.jspecify.annotations.NullMarked; diff --git a/core/src/main/java/org/springframework/security/access/hierarchicalroles/RoleHierarchyImpl.java b/core/src/main/java/org/springframework/security/access/hierarchicalroles/RoleHierarchyImpl.java index d1a28aec85..bf3a5d88ed 100755 --- a/core/src/main/java/org/springframework/security/access/hierarchicalroles/RoleHierarchyImpl.java +++ b/core/src/main/java/org/springframework/security/access/hierarchicalroles/RoleHierarchyImpl.java @@ -86,7 +86,7 @@ public final class RoleHierarchyImpl implements RoleHierarchy { * {@code rolesReachableInOneOrMoreStepsMap} is a Map that under the key of a specific * role name contains a set of all roles reachable from this role in 1 or more steps */ - private Map> rolesReachableInOneOrMoreStepsMap = null; + private final Map> rolesReachableInOneOrMoreStepsMap; private RoleHierarchyImpl(Map> hierarchy) { this.rolesReachableInOneOrMoreStepsMap = buildRolesReachableInOneOrMoreStepsMap(hierarchy); diff --git a/core/src/main/java/org/springframework/security/access/hierarchicalroles/package-info.java b/core/src/main/java/org/springframework/security/access/hierarchicalroles/package-info.java index e161c07e18..8048056d0a 100644 --- a/core/src/main/java/org/springframework/security/access/hierarchicalroles/package-info.java +++ b/core/src/main/java/org/springframework/security/access/hierarchicalroles/package-info.java @@ -17,4 +17,7 @@ /** * Role hierarchy implementation. */ +@NullMarked package org.springframework.security.access.hierarchicalroles; + +import org.jspecify.annotations.NullMarked; diff --git a/core/src/main/java/org/springframework/security/access/intercept/AbstractSecurityInterceptor.java b/core/src/main/java/org/springframework/security/access/intercept/AbstractSecurityInterceptor.java index 5582b2a145..4043af946a 100644 --- a/core/src/main/java/org/springframework/security/access/intercept/AbstractSecurityInterceptor.java +++ b/core/src/main/java/org/springframework/security/access/intercept/AbstractSecurityInterceptor.java @@ -22,6 +22,8 @@ import java.util.Set; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.NullUnmarked; +import org.jspecify.annotations.Nullable; import org.springframework.beans.factory.InitializingBean; import org.springframework.context.ApplicationEvent; @@ -114,6 +116,7 @@ import org.springframework.util.CollectionUtils; * {@link org.springframework.security.authorization.method.AuthorizationManagerAfterMethodInterceptor} * for method security. */ +@NullUnmarked @Deprecated public abstract class AbstractSecurityInterceptor implements InitializingBean, ApplicationEventPublisherAware, MessageSourceAware { @@ -125,11 +128,11 @@ public abstract class AbstractSecurityInterceptor private SecurityContextHolderStrategy securityContextHolderStrategy = SecurityContextHolder .getContextHolderStrategy(); - private ApplicationEventPublisher eventPublisher; + private @Nullable ApplicationEventPublisher eventPublisher; - private AccessDecisionManager accessDecisionManager; + private @Nullable AccessDecisionManager accessDecisionManager; - private AfterInvocationManager afterInvocationManager; + private @Nullable AfterInvocationManager afterInvocationManager; private AuthenticationManager authenticationManager = new NoOpAuthenticationManager(); @@ -190,7 +193,7 @@ public abstract class AbstractSecurityInterceptor } } - protected InterceptorStatusToken beforeInvocation(Object object) { + protected @Nullable InterceptorStatusToken beforeInvocation(Object object) { Assert.notNull(object, "Object was null"); if (!getSecureObjectClass().isAssignableFrom(object.getClass())) { throw new IllegalArgumentException("Security invocation attempted for object " + object.getClass().getName() @@ -291,7 +294,7 @@ public abstract class AbstractSecurityInterceptor * @return the object the secure object invocation should ultimately return to its * caller (may be null) */ - protected Object afterInvocation(InterceptorStatusToken token, Object returnedObject) { + protected Object afterInvocation(InterceptorStatusToken token, @Nullable Object returnedObject) { if (token == null) { // public object return returnedObject; diff --git a/core/src/main/java/org/springframework/security/access/intercept/AfterInvocationProviderManager.java b/core/src/main/java/org/springframework/security/access/intercept/AfterInvocationProviderManager.java index 4003aa4659..91f7f1cf16 100644 --- a/core/src/main/java/org/springframework/security/access/intercept/AfterInvocationProviderManager.java +++ b/core/src/main/java/org/springframework/security/access/intercept/AfterInvocationProviderManager.java @@ -22,6 +22,8 @@ import java.util.List; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.NullUnmarked; +import org.jspecify.annotations.Nullable; import org.springframework.beans.factory.InitializingBean; import org.springframework.core.log.LogMessage; @@ -52,12 +54,14 @@ import org.springframework.util.CollectionUtils; * @see org.springframework.security.authorization.method.AuthorizationManagerAfterMethodInterceptor * @deprecated Use delegation with {@link AuthorizationManager} */ +@NullUnmarked @Deprecated public class AfterInvocationProviderManager implements AfterInvocationManager, InitializingBean { protected static final Log logger = LogFactory.getLog(AfterInvocationProviderManager.class); - private List providers; + @SuppressWarnings("NullAway.Init") + private @Nullable List providers; @Override public void afterPropertiesSet() { diff --git a/core/src/main/java/org/springframework/security/access/intercept/MethodInvocationPrivilegeEvaluator.java b/core/src/main/java/org/springframework/security/access/intercept/MethodInvocationPrivilegeEvaluator.java index 8640ca08e9..1041c5080b 100644 --- a/core/src/main/java/org/springframework/security/access/intercept/MethodInvocationPrivilegeEvaluator.java +++ b/core/src/main/java/org/springframework/security/access/intercept/MethodInvocationPrivilegeEvaluator.java @@ -21,6 +21,8 @@ import java.util.Collection; import org.aopalliance.intercept.MethodInvocation; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.NullUnmarked; +import org.jspecify.annotations.Nullable; import org.springframework.beans.factory.InitializingBean; import org.springframework.core.log.LogMessage; @@ -46,12 +48,14 @@ import org.springframework.util.Assert; * @deprecated Use {@link org.springframework.security.authorization.AuthorizationManager} * instead */ +@NullUnmarked @Deprecated public class MethodInvocationPrivilegeEvaluator implements InitializingBean { protected static final Log logger = LogFactory.getLog(MethodInvocationPrivilegeEvaluator.class); - private AbstractSecurityInterceptor securityInterceptor; + @SuppressWarnings("NullAway.Init") + private @Nullable AbstractSecurityInterceptor securityInterceptor; @Override public void afterPropertiesSet() { diff --git a/core/src/main/java/org/springframework/security/access/intercept/NullRunAsManager.java b/core/src/main/java/org/springframework/security/access/intercept/NullRunAsManager.java index 36b8ad2157..819e3fb576 100644 --- a/core/src/main/java/org/springframework/security/access/intercept/NullRunAsManager.java +++ b/core/src/main/java/org/springframework/security/access/intercept/NullRunAsManager.java @@ -18,6 +18,9 @@ package org.springframework.security.access.intercept; import java.util.Collection; +import org.jspecify.annotations.NullUnmarked; +import org.jspecify.annotations.Nullable; + import org.springframework.security.access.ConfigAttribute; import org.springframework.security.core.Authentication; @@ -30,11 +33,13 @@ import org.springframework.security.core.Authentication; * @author Ben Alex * @deprecated please see {@link RunAsManager} deprecation notice */ +@NullUnmarked @Deprecated final class NullRunAsManager implements RunAsManager { @Override - public Authentication buildRunAs(Authentication authentication, Object object, Collection config) { + public @Nullable Authentication buildRunAs(Authentication authentication, Object object, + Collection config) { return null; } diff --git a/core/src/main/java/org/springframework/security/access/intercept/RunAsImplAuthenticationProvider.java b/core/src/main/java/org/springframework/security/access/intercept/RunAsImplAuthenticationProvider.java index c43dd2b17b..9dc1feacac 100644 --- a/core/src/main/java/org/springframework/security/access/intercept/RunAsImplAuthenticationProvider.java +++ b/core/src/main/java/org/springframework/security/access/intercept/RunAsImplAuthenticationProvider.java @@ -16,6 +16,9 @@ package org.springframework.security.access.intercept; +import org.jspecify.annotations.NullUnmarked; +import org.jspecify.annotations.Nullable; + import org.springframework.beans.factory.InitializingBean; import org.springframework.context.MessageSource; import org.springframework.context.MessageSourceAware; @@ -44,12 +47,14 @@ import org.springframework.util.Assert; * class is only used by now-deprecated components. There is not yet an equivalent * replacement in Spring Security. */ +@NullUnmarked @Deprecated public class RunAsImplAuthenticationProvider implements InitializingBean, AuthenticationProvider, MessageSourceAware { protected MessageSourceAccessor messages = SpringSecurityMessageSource.getAccessor(); - private String key; + @SuppressWarnings("NullAway.Init") + private @Nullable String key; @Override public void afterPropertiesSet() { diff --git a/core/src/main/java/org/springframework/security/access/intercept/RunAsManagerImpl.java b/core/src/main/java/org/springframework/security/access/intercept/RunAsManagerImpl.java index e02a4340fa..67627270be 100644 --- a/core/src/main/java/org/springframework/security/access/intercept/RunAsManagerImpl.java +++ b/core/src/main/java/org/springframework/security/access/intercept/RunAsManagerImpl.java @@ -20,6 +20,9 @@ import java.util.ArrayList; import java.util.Collection; import java.util.List; +import org.jspecify.annotations.NullUnmarked; +import org.jspecify.annotations.Nullable; + import org.springframework.beans.factory.InitializingBean; import org.springframework.security.access.ConfigAttribute; import org.springframework.security.core.Authentication; @@ -56,10 +59,12 @@ import org.springframework.util.Assert; * class is only used by now-deprecated components. There is not yet an equivalent * replacement in Spring Security. */ +@NullUnmarked @Deprecated public class RunAsManagerImpl implements RunAsManager, InitializingBean { - private String key; + @SuppressWarnings("NullAway.Init") + private @Nullable String key; private String rolePrefix = "ROLE_"; @@ -70,7 +75,7 @@ public class RunAsManagerImpl implements RunAsManager, InitializingBean { } @Override - public Authentication buildRunAs(Authentication authentication, Object object, + public @Nullable Authentication buildRunAs(Authentication authentication, Object object, Collection attributes) { List newAuthorities = new ArrayList<>(); for (ConfigAttribute attribute : attributes) { diff --git a/core/src/main/java/org/springframework/security/access/intercept/aopalliance/MethodSecurityInterceptor.java b/core/src/main/java/org/springframework/security/access/intercept/aopalliance/MethodSecurityInterceptor.java index 4287231eb5..ddd27addec 100644 --- a/core/src/main/java/org/springframework/security/access/intercept/aopalliance/MethodSecurityInterceptor.java +++ b/core/src/main/java/org/springframework/security/access/intercept/aopalliance/MethodSecurityInterceptor.java @@ -18,6 +18,8 @@ package org.springframework.security.access.intercept.aopalliance; import org.aopalliance.intercept.MethodInterceptor; import org.aopalliance.intercept.MethodInvocation; +import org.jspecify.annotations.NullUnmarked; +import org.jspecify.annotations.Nullable; import org.springframework.security.access.SecurityMetadataSource; import org.springframework.security.access.intercept.AbstractSecurityInterceptor; @@ -42,10 +44,11 @@ import org.springframework.security.access.method.MethodSecurityMetadataSource; * {@link org.springframework.security.authorization.method.AuthorizationManagerAfterMethodInterceptor} * instead */ +@NullUnmarked @Deprecated public class MethodSecurityInterceptor extends AbstractSecurityInterceptor implements MethodInterceptor { - private MethodSecurityMetadataSource securityMetadataSource; + private @Nullable MethodSecurityMetadataSource securityMetadataSource; @Override public Class getSecureObjectClass() { diff --git a/core/src/main/java/org/springframework/security/access/intercept/aopalliance/MethodSecurityMetadataSourceAdvisor.java b/core/src/main/java/org/springframework/security/access/intercept/aopalliance/MethodSecurityMetadataSourceAdvisor.java index 58174d9d1a..95275ba6c3 100644 --- a/core/src/main/java/org/springframework/security/access/intercept/aopalliance/MethodSecurityMetadataSourceAdvisor.java +++ b/core/src/main/java/org/springframework/security/access/intercept/aopalliance/MethodSecurityMetadataSourceAdvisor.java @@ -23,6 +23,8 @@ import java.lang.reflect.Method; import org.aopalliance.aop.Advice; import org.aopalliance.intercept.MethodInterceptor; +import org.jspecify.annotations.NullUnmarked; +import org.jspecify.annotations.Nullable; import org.springframework.aop.Pointcut; import org.springframework.aop.support.AbstractPointcutAdvisor; @@ -53,17 +55,18 @@ import org.springframework.util.CollectionUtils; * @author Luke Taylor * @deprecated Use {@link EnableMethodSecurity} or publish interceptors directly */ +@NullUnmarked @Deprecated @SuppressWarnings("serial") public class MethodSecurityMetadataSourceAdvisor extends AbstractPointcutAdvisor implements BeanFactoryAware { private transient MethodSecurityMetadataSource attributeSource; - private transient MethodInterceptor interceptor; + private transient @Nullable MethodInterceptor interceptor; private final Pointcut pointcut = new MethodSecurityMetadataSourcePointcut(); - private BeanFactory beanFactory; + private @Nullable BeanFactory beanFactory; private final String adviceBeanName; diff --git a/core/src/main/java/org/springframework/security/access/intercept/aopalliance/package-info.java b/core/src/main/java/org/springframework/security/access/intercept/aopalliance/package-info.java index ad8a9098ec..ae88010d3a 100644 --- a/core/src/main/java/org/springframework/security/access/intercept/aopalliance/package-info.java +++ b/core/src/main/java/org/springframework/security/access/intercept/aopalliance/package-info.java @@ -18,4 +18,7 @@ * Enforces security for AOP Alliance MethodInvocations, such as via Spring * AOP. */ +@NullMarked package org.springframework.security.access.intercept.aopalliance; + +import org.jspecify.annotations.NullMarked; diff --git a/core/src/main/java/org/springframework/security/access/intercept/aspectj/MethodInvocationAdapter.java b/core/src/main/java/org/springframework/security/access/intercept/aspectj/MethodInvocationAdapter.java index da29032f78..0ea450a338 100644 --- a/core/src/main/java/org/springframework/security/access/intercept/aspectj/MethodInvocationAdapter.java +++ b/core/src/main/java/org/springframework/security/access/intercept/aspectj/MethodInvocationAdapter.java @@ -23,6 +23,7 @@ import org.aopalliance.intercept.MethodInvocation; import org.aspectj.lang.JoinPoint; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.reflect.CodeSignature; +import org.jspecify.annotations.NullUnmarked; import org.springframework.util.Assert; @@ -35,6 +36,7 @@ import org.springframework.util.Assert; * @deprecated This class will be removed from the public API. See * `JoinPointMethodInvocation` in `spring-security-aspects` for its replacement */ +@NullUnmarked @Deprecated public final class MethodInvocationAdapter implements MethodInvocation { diff --git a/core/src/main/java/org/springframework/security/access/intercept/aspectj/package-info.java b/core/src/main/java/org/springframework/security/access/intercept/aspectj/package-info.java index cf16a6f843..ee7deeca44 100644 --- a/core/src/main/java/org/springframework/security/access/intercept/aspectj/package-info.java +++ b/core/src/main/java/org/springframework/security/access/intercept/aspectj/package-info.java @@ -18,4 +18,7 @@ * Enforces security for AspectJ JointPoints, delegating secure object * callbacks to the calling aspect. */ +@NullMarked package org.springframework.security.access.intercept.aspectj; + +import org.jspecify.annotations.NullMarked; diff --git a/core/src/main/java/org/springframework/security/access/intercept/package-info.java b/core/src/main/java/org/springframework/security/access/intercept/package-info.java index 68b24a7798..fb02497170 100644 --- a/core/src/main/java/org/springframework/security/access/intercept/package-info.java +++ b/core/src/main/java/org/springframework/security/access/intercept/package-info.java @@ -33,4 +33,7 @@ * an appropriate {@link org.springframework.security.access.SecurityMetadataSource} for * the type of resources the secure object represents. */ +@NullMarked package org.springframework.security.access.intercept; + +import org.jspecify.annotations.NullMarked; diff --git a/core/src/main/java/org/springframework/security/access/method/AbstractFallbackMethodSecurityMetadataSource.java b/core/src/main/java/org/springframework/security/access/method/AbstractFallbackMethodSecurityMetadataSource.java index ef2feb7505..a01a69fb95 100644 --- a/core/src/main/java/org/springframework/security/access/method/AbstractFallbackMethodSecurityMetadataSource.java +++ b/core/src/main/java/org/springframework/security/access/method/AbstractFallbackMethodSecurityMetadataSource.java @@ -20,6 +20,8 @@ import java.lang.reflect.Method; import java.util.Collection; import java.util.Collections; +import org.jspecify.annotations.Nullable; + import org.springframework.aop.support.AopUtils; import org.springframework.security.access.ConfigAttribute; import org.springframework.security.authorization.AuthorizationManager; @@ -52,7 +54,7 @@ import org.springframework.security.authorization.AuthorizationManager; public abstract class AbstractFallbackMethodSecurityMetadataSource extends AbstractMethodSecurityMetadataSource { @Override - public Collection getAttributes(Method method, Class targetClass) { + public Collection getAttributes(Method method, @Nullable Class targetClass) { // The method may be on an interface, but we need attributes from the target // class. // If the target class is null, the method will be unchanged. @@ -92,7 +94,7 @@ public abstract class AbstractFallbackMethodSecurityMetadataSource extends Abstr * @param targetClass the target class for the invocation (may be null) * @return the security metadata (or null if no metadata applies) */ - protected abstract Collection findAttributes(Method method, Class targetClass); + protected abstract Collection findAttributes(Method method, @Nullable Class targetClass); /** * Obtains the security metadata registered against the specified class. diff --git a/core/src/main/java/org/springframework/security/access/method/AbstractMethodSecurityMetadataSource.java b/core/src/main/java/org/springframework/security/access/method/AbstractMethodSecurityMetadataSource.java index c276ead8aa..4cf3729a5b 100644 --- a/core/src/main/java/org/springframework/security/access/method/AbstractMethodSecurityMetadataSource.java +++ b/core/src/main/java/org/springframework/security/access/method/AbstractMethodSecurityMetadataSource.java @@ -21,6 +21,7 @@ import java.util.Collection; import org.aopalliance.intercept.MethodInvocation; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.NullUnmarked; import org.springframework.aop.framework.AopProxyUtils; import org.springframework.security.access.ConfigAttribute; @@ -36,6 +37,7 @@ import org.springframework.security.authorization.AuthorizationManager; * {@code } and {@code } instead or use * annotation-based or {@link AuthorizationManager}-based authorization */ +@NullUnmarked @Deprecated public abstract class AbstractMethodSecurityMetadataSource implements MethodSecurityMetadataSource { diff --git a/core/src/main/java/org/springframework/security/access/method/DelegatingMethodSecurityMetadataSource.java b/core/src/main/java/org/springframework/security/access/method/DelegatingMethodSecurityMetadataSource.java index 3b4c5c2eec..b85c86013d 100644 --- a/core/src/main/java/org/springframework/security/access/method/DelegatingMethodSecurityMetadataSource.java +++ b/core/src/main/java/org/springframework/security/access/method/DelegatingMethodSecurityMetadataSource.java @@ -25,6 +25,8 @@ import java.util.List; import java.util.Map; import java.util.Set; +import org.jspecify.annotations.Nullable; + import org.springframework.core.log.LogMessage; import org.springframework.security.access.ConfigAttribute; import org.springframework.security.authorization.AuthorizationManager; @@ -57,7 +59,7 @@ public final class DelegatingMethodSecurityMetadataSource extends AbstractMethod } @Override - public Collection getAttributes(Method method, Class targetClass) { + public Collection getAttributes(Method method, @Nullable Class targetClass) { DefaultCacheKey cacheKey = new DefaultCacheKey(method, targetClass); synchronized (this.attributeCache) { Collection cached = this.attributeCache.get(cacheKey); @@ -104,9 +106,9 @@ public final class DelegatingMethodSecurityMetadataSource extends AbstractMethod private final Method method; - private final Class targetClass; + private final @Nullable Class targetClass; - DefaultCacheKey(Method method, Class targetClass) { + DefaultCacheKey(Method method, @Nullable Class targetClass) { this.method = method; this.targetClass = targetClass; } diff --git a/core/src/main/java/org/springframework/security/access/method/MapBasedMethodSecurityMetadataSource.java b/core/src/main/java/org/springframework/security/access/method/MapBasedMethodSecurityMetadataSource.java index b7ebebb540..3ebb24d141 100644 --- a/core/src/main/java/org/springframework/security/access/method/MapBasedMethodSecurityMetadataSource.java +++ b/core/src/main/java/org/springframework/security/access/method/MapBasedMethodSecurityMetadataSource.java @@ -25,6 +25,9 @@ import java.util.List; import java.util.Map; import java.util.Set; +import org.jspecify.annotations.NullUnmarked; +import org.jspecify.annotations.Nullable; + import org.springframework.beans.factory.BeanClassLoaderAware; import org.springframework.core.log.LogMessage; import org.springframework.security.access.ConfigAttribute; @@ -47,11 +50,13 @@ import org.springframework.util.ClassUtils; * {@code } and {@code } instead or use * annotation-based or {@link AuthorizationManager}-based authorization */ +@NullUnmarked @Deprecated public class MapBasedMethodSecurityMetadataSource extends AbstractFallbackMethodSecurityMetadataSource implements BeanClassLoaderAware { - private ClassLoader beanClassLoader = ClassUtils.getDefaultClassLoader(); + @SuppressWarnings("NullAway") + private @Nullable ClassLoader beanClassLoader = ClassUtils.getDefaultClassLoader(); /** * Map from RegisteredMethod to ConfigAttribute list @@ -80,7 +85,7 @@ public class MapBasedMethodSecurityMetadataSource extends AbstractFallbackMethod * Implementation does not support class-level attributes. */ @Override - protected Collection findAttributes(Class clazz) { + protected @Nullable Collection findAttributes(Class clazz) { return null; } @@ -89,14 +94,14 @@ public class MapBasedMethodSecurityMetadataSource extends AbstractFallbackMethod * applicable. */ @Override - protected Collection findAttributes(Method method, Class targetClass) { + protected @Nullable Collection findAttributes(Method method, Class targetClass) { if (targetClass == null) { return null; } return findAttributesSpecifiedAgainst(method, targetClass); } - private List findAttributesSpecifiedAgainst(Method method, Class clazz) { + private @Nullable List findAttributesSpecifiedAgainst(Method method, Class clazz) { RegisteredMethod registeredMethod = new RegisteredMethod(method, clazz); if (this.methodMap.containsKey(registeredMethod)) { return this.methodMap.get(registeredMethod); diff --git a/core/src/main/java/org/springframework/security/access/method/MethodSecurityMetadataSource.java b/core/src/main/java/org/springframework/security/access/method/MethodSecurityMetadataSource.java index 1512415f0e..4d875118c4 100644 --- a/core/src/main/java/org/springframework/security/access/method/MethodSecurityMetadataSource.java +++ b/core/src/main/java/org/springframework/security/access/method/MethodSecurityMetadataSource.java @@ -19,6 +19,8 @@ package org.springframework.security.access.method; import java.lang.reflect.Method; import java.util.Collection; +import org.jspecify.annotations.Nullable; + import org.springframework.security.access.ConfigAttribute; import org.springframework.security.access.SecurityMetadataSource; import org.springframework.security.authorization.AuthorizationManager; @@ -37,6 +39,6 @@ import org.springframework.security.authorization.AuthorizationManager; @Deprecated public interface MethodSecurityMetadataSource extends SecurityMetadataSource { - Collection getAttributes(Method method, Class targetClass); + Collection getAttributes(Method method, @Nullable Class targetClass); } diff --git a/core/src/main/java/org/springframework/security/access/method/package-info.java b/core/src/main/java/org/springframework/security/access/method/package-info.java index d7cf84ccdc..a97a74b2a1 100644 --- a/core/src/main/java/org/springframework/security/access/method/package-info.java +++ b/core/src/main/java/org/springframework/security/access/method/package-info.java @@ -18,4 +18,7 @@ * Provides {@code SecurityMetadataSource} implementations for securing Java method * invocations via different AOP libraries. */ +@NullMarked package org.springframework.security.access.method; + +import org.jspecify.annotations.NullMarked; diff --git a/core/src/main/java/org/springframework/security/access/package-info.java b/core/src/main/java/org/springframework/security/access/package-info.java index 2055530ce8..997d852977 100644 --- a/core/src/main/java/org/springframework/security/access/package-info.java +++ b/core/src/main/java/org/springframework/security/access/package-info.java @@ -21,4 +21,7 @@ * {@link org.springframework.security.access.AccessDecisionManager AccessDecisionManager} * interface. */ +@NullMarked package org.springframework.security.access; + +import org.jspecify.annotations.NullMarked; diff --git a/core/src/main/java/org/springframework/security/access/prepost/PostInvocationAdviceProvider.java b/core/src/main/java/org/springframework/security/access/prepost/PostInvocationAdviceProvider.java index f974735509..f8dfc1cc9f 100644 --- a/core/src/main/java/org/springframework/security/access/prepost/PostInvocationAdviceProvider.java +++ b/core/src/main/java/org/springframework/security/access/prepost/PostInvocationAdviceProvider.java @@ -21,6 +21,8 @@ import java.util.Collection; import org.aopalliance.intercept.MethodInvocation; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.NullUnmarked; +import org.jspecify.annotations.Nullable; import org.springframework.security.access.AccessDeniedException; import org.springframework.security.access.AfterInvocationProvider; @@ -40,6 +42,7 @@ import org.springframework.security.core.Authentication; * {@link org.springframework.security.authorization.method.AuthorizationManagerAfterMethodInterceptor} * instead */ +@NullUnmarked @Deprecated public class PostInvocationAdviceProvider implements AfterInvocationProvider { @@ -62,7 +65,7 @@ public class PostInvocationAdviceProvider implements AfterInvocationProvider { returnedObject); } - private PostInvocationAttribute findPostInvocationAttribute(Collection config) { + private @Nullable PostInvocationAttribute findPostInvocationAttribute(Collection config) { for (ConfigAttribute attribute : config) { if (attribute instanceof PostInvocationAttribute) { return (PostInvocationAttribute) attribute; diff --git a/core/src/main/java/org/springframework/security/access/prepost/PreInvocationAuthorizationAdviceVoter.java b/core/src/main/java/org/springframework/security/access/prepost/PreInvocationAuthorizationAdviceVoter.java index 9a94601e1d..101ae67680 100644 --- a/core/src/main/java/org/springframework/security/access/prepost/PreInvocationAuthorizationAdviceVoter.java +++ b/core/src/main/java/org/springframework/security/access/prepost/PreInvocationAuthorizationAdviceVoter.java @@ -21,6 +21,8 @@ import java.util.Collection; import org.aopalliance.intercept.MethodInvocation; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.NullUnmarked; +import org.jspecify.annotations.Nullable; import org.springframework.security.access.AccessDecisionVoter; import org.springframework.security.access.ConfigAttribute; @@ -42,6 +44,7 @@ import org.springframework.security.core.Authentication; * {@link org.springframework.security.authorization.method.AuthorizationManagerBeforeMethodInterceptor} * instead */ +@NullUnmarked @Deprecated public class PreInvocationAuthorizationAdviceVoter implements AccessDecisionVoter { @@ -75,7 +78,7 @@ public class PreInvocationAuthorizationAdviceVoter implements AccessDecisionVote return this.preAdvice.before(authentication, method, preAttr) ? ACCESS_GRANTED : ACCESS_DENIED; } - private PreInvocationAttribute findPreInvocationAttribute(Collection config) { + private @Nullable PreInvocationAttribute findPreInvocationAttribute(Collection config) { for (ConfigAttribute attribute : config) { if (attribute instanceof PreInvocationAttribute) { return (PreInvocationAttribute) attribute; diff --git a/core/src/main/java/org/springframework/security/access/prepost/PrePostAdviceReactiveMethodInterceptor.java b/core/src/main/java/org/springframework/security/access/prepost/PrePostAdviceReactiveMethodInterceptor.java index 66e2e02f26..de96bf008a 100644 --- a/core/src/main/java/org/springframework/security/access/prepost/PrePostAdviceReactiveMethodInterceptor.java +++ b/core/src/main/java/org/springframework/security/access/prepost/PrePostAdviceReactiveMethodInterceptor.java @@ -22,6 +22,8 @@ import java.util.Collection; import kotlinx.coroutines.reactive.ReactiveFlowKt; import org.aopalliance.intercept.MethodInterceptor; import org.aopalliance.intercept.MethodInvocation; +import org.jspecify.annotations.NullUnmarked; +import org.jspecify.annotations.Nullable; import org.reactivestreams.Publisher; import reactor.core.Exceptions; import reactor.core.publisher.Flux; @@ -54,6 +56,7 @@ import org.springframework.util.Assert; * or * {@link org.springframework.security.authorization.method.AuthorizationManagerAfterReactiveMethodInterceptor} */ +@NullUnmarked @Deprecated public class PrePostAdviceReactiveMethodInterceptor implements MethodInterceptor { @@ -142,7 +145,7 @@ public class PrePostAdviceReactiveMethodInterceptor implements MethodInterceptor .map((r) -> (attr != null) ? this.postAdvice.after(auth, invocation, attr, r) : r)); } - private static > T proceed(final MethodInvocation invocation) { + private static > @Nullable T proceed(final MethodInvocation invocation) { try { return (T) invocation.proceed(); } @@ -151,7 +154,7 @@ public class PrePostAdviceReactiveMethodInterceptor implements MethodInterceptor } } - private static Object flowProceed(final MethodInvocation invocation) { + private static @Nullable Object flowProceed(final MethodInvocation invocation) { try { return invocation.proceed(); } @@ -160,7 +163,7 @@ public class PrePostAdviceReactiveMethodInterceptor implements MethodInterceptor } } - private static PostInvocationAttribute findPostInvocationAttribute(Collection config) { + private static @Nullable PostInvocationAttribute findPostInvocationAttribute(Collection config) { for (ConfigAttribute attribute : config) { if (attribute instanceof PostInvocationAttribute) { return (PostInvocationAttribute) attribute; @@ -169,7 +172,7 @@ public class PrePostAdviceReactiveMethodInterceptor implements MethodInterceptor return null; } - private static PreInvocationAttribute findPreInvocationAttribute(Collection config) { + private static @Nullable PreInvocationAttribute findPreInvocationAttribute(Collection config) { for (ConfigAttribute attribute : config) { if (attribute instanceof PreInvocationAttribute) { return (PreInvocationAttribute) attribute; diff --git a/core/src/main/java/org/springframework/security/access/prepost/PrePostAnnotationSecurityMetadataSource.java b/core/src/main/java/org/springframework/security/access/prepost/PrePostAnnotationSecurityMetadataSource.java index 1db7d1cb9c..5a2061b9b8 100644 --- a/core/src/main/java/org/springframework/security/access/prepost/PrePostAnnotationSecurityMetadataSource.java +++ b/core/src/main/java/org/springframework/security/access/prepost/PrePostAnnotationSecurityMetadataSource.java @@ -22,6 +22,9 @@ import java.util.ArrayList; import java.util.Collection; import java.util.Collections; +import org.jspecify.annotations.NullUnmarked; +import org.jspecify.annotations.Nullable; + import org.springframework.core.annotation.AnnotationUtils; import org.springframework.core.log.LogMessage; import org.springframework.security.access.ConfigAttribute; @@ -54,6 +57,7 @@ import org.springframework.util.ClassUtils; * {@link org.springframework.security.authorization.method.PostAuthorizeAuthorizationManager} * instead */ +@NullUnmarked @Deprecated public class PrePostAnnotationSecurityMetadataSource extends AbstractMethodSecurityMetadataSource { @@ -98,7 +102,7 @@ public class PrePostAnnotationSecurityMetadataSource extends AbstractMethodSecur } @Override - public Collection getAllConfigAttributes() { + public @Nullable Collection getAllConfigAttributes() { return null; } @@ -108,7 +112,8 @@ public class PrePostAnnotationSecurityMetadataSource extends AbstractMethodSecur * for the logic of this method. The ordering here is slightly different in that we * consider method-specific annotations on an interface before class-level ones. */ - private A findAnnotation(Method method, Class targetClass, Class annotationClass) { + private @Nullable A findAnnotation(Method method, Class targetClass, + Class annotationClass) { // The method may be on an interface, but we need attributes from the target // class. // If the target class is null, the method will be unchanged. diff --git a/core/src/main/java/org/springframework/security/access/prepost/PrePostInvocationAttributeFactory.java b/core/src/main/java/org/springframework/security/access/prepost/PrePostInvocationAttributeFactory.java index 6c428f1050..953448c926 100644 --- a/core/src/main/java/org/springframework/security/access/prepost/PrePostInvocationAttributeFactory.java +++ b/core/src/main/java/org/springframework/security/access/prepost/PrePostInvocationAttributeFactory.java @@ -16,6 +16,8 @@ package org.springframework.security.access.prepost; +import org.jspecify.annotations.Nullable; + import org.springframework.aop.framework.AopInfrastructureBean; import org.springframework.security.authorization.AuthorizationManager; @@ -29,9 +31,10 @@ import org.springframework.security.authorization.AuthorizationManager; @Deprecated public interface PrePostInvocationAttributeFactory extends AopInfrastructureBean { - PreInvocationAttribute createPreInvocationAttribute(String preFilterAttribute, String filterObject, - String preAuthorizeAttribute); + PreInvocationAttribute createPreInvocationAttribute(@Nullable String preFilterAttribute, + @Nullable String filterObject, @Nullable String preAuthorizeAttribute); - PostInvocationAttribute createPostInvocationAttribute(String postFilterAttribute, String postAuthorizeAttribute); + PostInvocationAttribute createPostInvocationAttribute(@Nullable String postFilterAttribute, + @Nullable String postAuthorizeAttribute); } diff --git a/core/src/main/java/org/springframework/security/access/prepost/package-info.java b/core/src/main/java/org/springframework/security/access/prepost/package-info.java index 6fc3a3a8f9..2303bc74eb 100644 --- a/core/src/main/java/org/springframework/security/access/prepost/package-info.java +++ b/core/src/main/java/org/springframework/security/access/prepost/package-info.java @@ -21,4 +21,7 @@ * Other than the annotations themselves, the classes should be regarded as for internal * framework use and are liable to change without notice. */ +@NullMarked package org.springframework.security.access.prepost; + +import org.jspecify.annotations.NullMarked; diff --git a/core/src/main/java/org/springframework/security/access/vote/AbstractAclVoter.java b/core/src/main/java/org/springframework/security/access/vote/AbstractAclVoter.java index 8417d368e4..ec13bdb626 100644 --- a/core/src/main/java/org/springframework/security/access/vote/AbstractAclVoter.java +++ b/core/src/main/java/org/springframework/security/access/vote/AbstractAclVoter.java @@ -17,6 +17,8 @@ package org.springframework.security.access.vote; import org.aopalliance.intercept.MethodInvocation; +import org.jspecify.annotations.NullUnmarked; +import org.jspecify.annotations.Nullable; import org.springframework.security.access.AccessDecisionVoter; import org.springframework.security.access.AuthorizationServiceException; @@ -30,10 +32,12 @@ import org.springframework.util.Assert; * @deprecated Now used by only-deprecated classes. Generally speaking, in-memory ACL is * no longer advised, so no replacement is planned at this point. */ +@NullUnmarked @Deprecated public abstract class AbstractAclVoter implements AccessDecisionVoter { - private Class processDomainObjectClass; + @SuppressWarnings("NullAway.Init") + private @Nullable Class processDomainObjectClass; protected Object getDomainObjectInstance(MethodInvocation invocation) { Object[] args = invocation.getArguments(); diff --git a/core/src/main/java/org/springframework/security/access/vote/RoleHierarchyVoter.java b/core/src/main/java/org/springframework/security/access/vote/RoleHierarchyVoter.java index 4a923557a6..2a36084c0d 100644 --- a/core/src/main/java/org/springframework/security/access/vote/RoleHierarchyVoter.java +++ b/core/src/main/java/org/springframework/security/access/vote/RoleHierarchyVoter.java @@ -18,6 +18,9 @@ package org.springframework.security.access.vote; import java.util.Collection; +import org.jspecify.annotations.NullUnmarked; +import org.jspecify.annotations.Nullable; + import org.springframework.security.access.hierarchicalroles.RoleHierarchy; import org.springframework.security.core.Authentication; import org.springframework.security.core.GrantedAuthority; @@ -33,10 +36,12 @@ import org.springframework.util.Assert; * {@link org.springframework.security.authorization.AuthorityAuthorizationManager#setRoleHierarchy} * instead */ +@NullUnmarked @Deprecated public class RoleHierarchyVoter extends RoleVoter { - private RoleHierarchy roleHierarchy = null; + @SuppressWarnings("NullAway") + private @Nullable RoleHierarchy roleHierarchy = null; public RoleHierarchyVoter(RoleHierarchy roleHierarchy) { Assert.notNull(roleHierarchy, "RoleHierarchy must not be null"); diff --git a/core/src/main/java/org/springframework/security/access/vote/package-info.java b/core/src/main/java/org/springframework/security/access/vote/package-info.java index 2ec2ecedb2..923a6bed70 100644 --- a/core/src/main/java/org/springframework/security/access/vote/package-info.java +++ b/core/src/main/java/org/springframework/security/access/vote/package-info.java @@ -17,4 +17,7 @@ /** * Implements a vote-based approach to authorization decisions. */ +@NullMarked package org.springframework.security.access.vote; + +import org.jspecify.annotations.NullMarked; diff --git a/core/src/main/java/org/springframework/security/aot/hint/CoreSecurityRuntimeHints.java b/core/src/main/java/org/springframework/security/aot/hint/CoreSecurityRuntimeHints.java index 2ac11b0f87..72e6026b30 100644 --- a/core/src/main/java/org/springframework/security/aot/hint/CoreSecurityRuntimeHints.java +++ b/core/src/main/java/org/springframework/security/aot/hint/CoreSecurityRuntimeHints.java @@ -19,6 +19,8 @@ package org.springframework.security.aot.hint; import java.util.List; import java.util.stream.Stream; +import org.jspecify.annotations.Nullable; + import org.springframework.aot.hint.MemberCategory; import org.springframework.aot.hint.RuntimeHints; import org.springframework.aot.hint.RuntimeHintsRegistrar; @@ -54,7 +56,7 @@ import org.springframework.security.core.userdetails.jdbc.JdbcDaoImpl; class CoreSecurityRuntimeHints implements RuntimeHintsRegistrar { @Override - public void registerHints(RuntimeHints hints, ClassLoader classLoader) { + public void registerHints(RuntimeHints hints, @Nullable ClassLoader classLoader) { registerExceptionEventsHints(hints); registerExpressionEvaluationHints(hints); registerMethodSecurityHints(hints); diff --git a/core/src/main/java/org/springframework/security/aot/hint/OneTimeTokenRuntimeHints.java b/core/src/main/java/org/springframework/security/aot/hint/OneTimeTokenRuntimeHints.java index 5dd7ddb3ef..3055fdfb84 100644 --- a/core/src/main/java/org/springframework/security/aot/hint/OneTimeTokenRuntimeHints.java +++ b/core/src/main/java/org/springframework/security/aot/hint/OneTimeTokenRuntimeHints.java @@ -16,6 +16,8 @@ package org.springframework.security.aot.hint; +import org.jspecify.annotations.Nullable; + import org.springframework.aot.hint.RuntimeHints; import org.springframework.aot.hint.RuntimeHintsRegistrar; import org.springframework.jdbc.core.JdbcOperations; @@ -33,7 +35,7 @@ import org.springframework.security.authentication.ott.OneTimeTokenService; class OneTimeTokenRuntimeHints implements RuntimeHintsRegistrar { @Override - public void registerHints(RuntimeHints hints, ClassLoader classLoader) { + public void registerHints(RuntimeHints hints, @Nullable ClassLoader classLoader) { hints.resources().registerPattern("org/springframework/security/core/ott/jdbc/one-time-tokens-schema.sql"); } diff --git a/core/src/main/java/org/springframework/security/aot/hint/package-info.java b/core/src/main/java/org/springframework/security/aot/hint/package-info.java new file mode 100644 index 0000000000..690f3fc2d0 --- /dev/null +++ b/core/src/main/java/org/springframework/security/aot/hint/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright 2002-2016 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.aot.hint; + +import org.jspecify.annotations.NullMarked; diff --git a/core/src/main/java/org/springframework/security/authentication/AbstractAuthenticationToken.java b/core/src/main/java/org/springframework/security/authentication/AbstractAuthenticationToken.java index d69483354f..7ec4167160 100644 --- a/core/src/main/java/org/springframework/security/authentication/AbstractAuthenticationToken.java +++ b/core/src/main/java/org/springframework/security/authentication/AbstractAuthenticationToken.java @@ -21,6 +21,8 @@ import java.util.ArrayList; import java.util.Collection; import java.util.Collections; +import org.jspecify.annotations.Nullable; + import org.springframework.security.core.AuthenticatedPrincipal; import org.springframework.security.core.Authentication; import org.springframework.security.core.CredentialsContainer; @@ -41,7 +43,7 @@ public abstract class AbstractAuthenticationToken implements Authentication, Cre private final Collection authorities; - private Object details; + private @Nullable Object details; private boolean authenticated = false; @@ -50,7 +52,7 @@ public abstract class AbstractAuthenticationToken implements Authentication, Cre * @param authorities the collection of GrantedAuthoritys for the principal * represented by this authentication object. */ - public AbstractAuthenticationToken(Collection authorities) { + public AbstractAuthenticationToken(@Nullable Collection authorities) { if (authorities == null) { this.authorities = AuthorityUtils.NO_AUTHORITIES; return; @@ -91,11 +93,11 @@ public abstract class AbstractAuthenticationToken implements Authentication, Cre } @Override - public Object getDetails() { + public @Nullable Object getDetails() { return this.details; } - public void setDetails(Object details) { + public void setDetails(@Nullable Object details) { this.details = details; } @@ -111,7 +113,7 @@ public abstract class AbstractAuthenticationToken implements Authentication, Cre eraseSecret(this.details); } - private void eraseSecret(Object secret) { + private void eraseSecret(@Nullable Object secret) { if (secret instanceof CredentialsContainer container) { container.eraseCredentials(); } diff --git a/core/src/main/java/org/springframework/security/authentication/AbstractUserDetailsReactiveAuthenticationManager.java b/core/src/main/java/org/springframework/security/authentication/AbstractUserDetailsReactiveAuthenticationManager.java index ba2cc7fef9..bfe9afbc2c 100644 --- a/core/src/main/java/org/springframework/security/authentication/AbstractUserDetailsReactiveAuthenticationManager.java +++ b/core/src/main/java/org/springframework/security/authentication/AbstractUserDetailsReactiveAuthenticationManager.java @@ -18,6 +18,7 @@ package org.springframework.security.authentication; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.Nullable; import reactor.core.publisher.Mono; import reactor.core.scheduler.Scheduler; import reactor.core.scheduler.Schedulers; @@ -60,7 +61,7 @@ public abstract class AbstractUserDetailsReactiveAuthenticationManager private PasswordEncoder passwordEncoder = PasswordEncoderFactories.createDelegatingPasswordEncoder(); - private ReactiveUserDetailsPasswordService userDetailsPasswordService; + private ReactiveUserDetailsPasswordService userDetailsPasswordService = ReactiveUserDetailsPasswordService.NOOP; private Scheduler scheduler = Schedulers.boundedElastic(); @@ -68,7 +69,7 @@ public abstract class AbstractUserDetailsReactiveAuthenticationManager private UserDetailsChecker postAuthenticationChecks = this::defaultPostAuthenticationChecks; - private ReactiveCompromisedPasswordChecker compromisedPasswordChecker; + private @Nullable ReactiveCompromisedPasswordChecker compromisedPasswordChecker; private void defaultPreAuthenticationChecks(UserDetails user) { if (!user.isAccountNonLocked()) { @@ -99,7 +100,8 @@ public abstract class AbstractUserDetailsReactiveAuthenticationManager @Override public Mono authenticate(Authentication authentication) { String username = authentication.getName(); - String presentedPassword = (String) authentication.getCredentials(); + String presentedPassword = (authentication.getCredentials() != null) + ? authentication.getCredentials().toString() : null; // @formatter:off return retrieveUser(username) .doOnNext(this.preAuthenticationChecks::check) @@ -113,7 +115,7 @@ public abstract class AbstractUserDetailsReactiveAuthenticationManager // @formatter:on } - private Mono checkCompromisedPassword(String password) { + private Mono checkCompromisedPassword(@Nullable String password) { if (this.compromisedPasswordChecker == null) { return Mono.empty(); } @@ -123,9 +125,10 @@ public abstract class AbstractUserDetailsReactiveAuthenticationManager "The provided password is compromised, please change your password"))); } - private Mono upgradeEncodingIfNecessary(UserDetails userDetails, String presentedPassword) { - boolean upgradeEncoding = this.userDetailsPasswordService != null - && this.passwordEncoder.upgradeEncoding(userDetails.getPassword()); + private Mono upgradeEncodingIfNecessary(UserDetails userDetails, @Nullable String presentedPassword) { + String existingEncodedPassword = userDetails.getPassword(); + boolean upgradeEncoding = existingEncodedPassword != null + && this.passwordEncoder.upgradeEncoding(existingEncodedPassword); if (upgradeEncoding) { String newPassword = this.passwordEncoder.encode(presentedPassword); return this.userDetailsPasswordService.updatePassword(userDetails, newPassword); @@ -170,6 +173,7 @@ public abstract class AbstractUserDetailsReactiveAuthenticationManager * @param userDetailsPasswordService the service to use */ public void setUserDetailsPasswordService(ReactiveUserDetailsPasswordService userDetailsPasswordService) { + Assert.notNull(userDetailsPasswordService, "userDetailsPasswordService cannot be null"); this.userDetailsPasswordService = userDetailsPasswordService; } diff --git a/core/src/main/java/org/springframework/security/authentication/AnonymousAuthenticationProvider.java b/core/src/main/java/org/springframework/security/authentication/AnonymousAuthenticationProvider.java index dbff14cb4c..5f370d9df7 100644 --- a/core/src/main/java/org/springframework/security/authentication/AnonymousAuthenticationProvider.java +++ b/core/src/main/java/org/springframework/security/authentication/AnonymousAuthenticationProvider.java @@ -16,6 +16,8 @@ package org.springframework.security.authentication; +import org.jspecify.annotations.Nullable; + import org.springframework.context.MessageSource; import org.springframework.context.MessageSourceAware; import org.springframework.context.support.MessageSourceAccessor; @@ -45,7 +47,7 @@ public class AnonymousAuthenticationProvider implements AuthenticationProvider, } @Override - public Authentication authenticate(Authentication authentication) throws AuthenticationException { + public @Nullable Authentication authenticate(Authentication authentication) throws AuthenticationException { if (!supports(authentication.getClass())) { return null; } diff --git a/core/src/main/java/org/springframework/security/authentication/AuthenticationObservationContext.java b/core/src/main/java/org/springframework/security/authentication/AuthenticationObservationContext.java index 7756506a08..614afdf70d 100644 --- a/core/src/main/java/org/springframework/security/authentication/AuthenticationObservationContext.java +++ b/core/src/main/java/org/springframework/security/authentication/AuthenticationObservationContext.java @@ -17,6 +17,7 @@ package org.springframework.security.authentication; import io.micrometer.observation.Observation; +import org.jspecify.annotations.Nullable; import org.springframework.security.core.Authentication; import org.springframework.util.Assert; @@ -29,17 +30,17 @@ import org.springframework.util.Assert; */ public class AuthenticationObservationContext extends Observation.Context { - private Authentication authenticationRequest; + private @Nullable Authentication authenticationRequest; - private Class authenticationManager; + private @Nullable Class authenticationManager; - private Authentication authenticationResult; + private @Nullable Authentication authenticationResult; /** * Get the {@link Authentication} request that was observed * @return the observed {@link Authentication} request */ - public Authentication getAuthenticationRequest() { + public @Nullable Authentication getAuthenticationRequest() { return this.authenticationRequest; } @@ -60,7 +61,7 @@ public class AuthenticationObservationContext extends Observation.Context { * observed. In that case, this returns {@code null}. * @return any observed {@link Authentication} result, {@code null} otherwise */ - public Authentication getAuthenticationResult() { + public @Nullable Authentication getAuthenticationResult() { return this.authenticationResult; } @@ -76,7 +77,7 @@ public class AuthenticationObservationContext extends Observation.Context { * Get the {@link AuthenticationManager} class that processed the authentication * @return the observed {@link AuthenticationManager} class */ - public Class getAuthenticationManagerClass() { + public @Nullable Class getAuthenticationManagerClass() { return this.authenticationManager; } diff --git a/core/src/main/java/org/springframework/security/authentication/AuthenticationObservationConvention.java b/core/src/main/java/org/springframework/security/authentication/AuthenticationObservationConvention.java index 4d0b6c693d..056c526007 100644 --- a/core/src/main/java/org/springframework/security/authentication/AuthenticationObservationConvention.java +++ b/core/src/main/java/org/springframework/security/authentication/AuthenticationObservationConvention.java @@ -21,9 +21,7 @@ import java.util.Locale; import io.micrometer.common.KeyValues; import io.micrometer.observation.Observation; import io.micrometer.observation.ObservationConvention; -import org.jetbrains.annotations.NotNull; - -import org.springframework.lang.NonNull; +import org.jspecify.annotations.NonNull; /** * An {@link ObservationConvention} for translating authentications into @@ -63,9 +61,8 @@ public final class AuthenticationObservationConvention /** * {@inheritDoc} */ - @NotNull @Override - public KeyValues getLowCardinalityKeyValues(@NonNull AuthenticationObservationContext context) { + public @NonNull KeyValues getLowCardinalityKeyValues(@NonNull AuthenticationObservationContext context) { return KeyValues.of("authentication.request.type", getAuthenticationType(context)) .and("authentication.method", getAuthenticationMethod(context)) .and("authentication.result.type", getAuthenticationResult(context)) @@ -104,7 +101,7 @@ public final class AuthenticationObservationConvention * {@inheritDoc} */ @Override - public boolean supportsContext(@NotNull Observation.Context context) { + public boolean supportsContext(Observation.Context context) { return context instanceof AuthenticationObservationContext; } diff --git a/core/src/main/java/org/springframework/security/authentication/AuthenticationProvider.java b/core/src/main/java/org/springframework/security/authentication/AuthenticationProvider.java index 1659bcf375..cf304f8d69 100644 --- a/core/src/main/java/org/springframework/security/authentication/AuthenticationProvider.java +++ b/core/src/main/java/org/springframework/security/authentication/AuthenticationProvider.java @@ -16,6 +16,8 @@ package org.springframework.security.authentication; +import org.jspecify.annotations.Nullable; + import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; @@ -39,7 +41,7 @@ public interface AuthenticationProvider { * Authentication class will be tried. * @throws AuthenticationException if authentication fails. */ - Authentication authenticate(Authentication authentication) throws AuthenticationException; + @Nullable Authentication authenticate(Authentication authentication) throws AuthenticationException; /** * Returns true if this AuthenticationProvider supports the diff --git a/core/src/main/java/org/springframework/security/authentication/AuthenticationServiceException.java b/core/src/main/java/org/springframework/security/authentication/AuthenticationServiceException.java index 3bd076dfd8..64d619c98e 100644 --- a/core/src/main/java/org/springframework/security/authentication/AuthenticationServiceException.java +++ b/core/src/main/java/org/springframework/security/authentication/AuthenticationServiceException.java @@ -18,6 +18,8 @@ package org.springframework.security.authentication; import java.io.Serial; +import org.jspecify.annotations.Nullable; + import org.springframework.security.core.AuthenticationException; /** @@ -49,7 +51,7 @@ public class AuthenticationServiceException extends AuthenticationException { * @param msg the detail message * @param cause root cause */ - public AuthenticationServiceException(String msg, Throwable cause) { + public AuthenticationServiceException(@Nullable String msg, Throwable cause) { super(msg, cause); } diff --git a/core/src/main/java/org/springframework/security/authentication/DefaultAuthenticationEventPublisher.java b/core/src/main/java/org/springframework/security/authentication/DefaultAuthenticationEventPublisher.java index 4a509b08c4..f7de853147 100644 --- a/core/src/main/java/org/springframework/security/authentication/DefaultAuthenticationEventPublisher.java +++ b/core/src/main/java/org/springframework/security/authentication/DefaultAuthenticationEventPublisher.java @@ -24,6 +24,7 @@ import java.util.Properties; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.Nullable; import org.springframework.context.ApplicationEventPublisher; import org.springframework.context.ApplicationEventPublisherAware; @@ -71,13 +72,15 @@ public class DefaultAuthenticationEventPublisher private final HashMap> exceptionMappings = new HashMap<>(); - private Constructor defaultAuthenticationFailureEventConstructor; + private @Nullable Constructor defaultAuthenticationFailureEventConstructor; public DefaultAuthenticationEventPublisher() { - this(null); + this((event) -> { + }); } public DefaultAuthenticationEventPublisher(ApplicationEventPublisher applicationEventPublisher) { + Assert.notNull(applicationEventPublisher, "applicationEventPublisher cannot be null"); this.applicationEventPublisher = applicationEventPublisher; addMapping(BadCredentialsException.class.getName(), AuthenticationFailureBadCredentialsEvent.class); addMapping(UsernameNotFoundException.class.getName(), AuthenticationFailureBadCredentialsEvent.class); @@ -123,7 +126,8 @@ public class DefaultAuthenticationEventPublisher } } - private Constructor getEventConstructor(AuthenticationException exception) { + private @Nullable Constructor getEventConstructor( + AuthenticationException exception) { Constructor eventConstructor = this.exceptionMappings .get(exception.getClass().getName()); return (eventConstructor != null) ? eventConstructor : this.defaultAuthenticationFailureEventConstructor; diff --git a/core/src/main/java/org/springframework/security/authentication/InternalAuthenticationServiceException.java b/core/src/main/java/org/springframework/security/authentication/InternalAuthenticationServiceException.java index de59b2d5ef..2b2da3d368 100644 --- a/core/src/main/java/org/springframework/security/authentication/InternalAuthenticationServiceException.java +++ b/core/src/main/java/org/springframework/security/authentication/InternalAuthenticationServiceException.java @@ -18,6 +18,8 @@ package org.springframework.security.authentication; import java.io.Serial; +import org.jspecify.annotations.Nullable; + /** *

* Thrown if an authentication request could not be processed due to a system problem that @@ -42,7 +44,7 @@ public class InternalAuthenticationServiceException extends AuthenticationServic @Serial private static final long serialVersionUID = -6029644854192497840L; - public InternalAuthenticationServiceException(String message, Throwable cause) { + public InternalAuthenticationServiceException(@Nullable String message, Throwable cause) { super(message, cause); } diff --git a/core/src/main/java/org/springframework/security/authentication/ObservationAuthenticationManager.java b/core/src/main/java/org/springframework/security/authentication/ObservationAuthenticationManager.java index d6a021c6b3..bd21277074 100644 --- a/core/src/main/java/org/springframework/security/authentication/ObservationAuthenticationManager.java +++ b/core/src/main/java/org/springframework/security/authentication/ObservationAuthenticationManager.java @@ -46,6 +46,7 @@ public final class ObservationAuthenticationManager implements AuthenticationMan } @Override + @SuppressWarnings("NullAway") // Dataflow analysis limitation public Authentication authenticate(Authentication authentication) throws AuthenticationException { AuthenticationObservationContext context = new AuthenticationObservationContext(); context.setAuthenticationRequest(authentication); diff --git a/core/src/main/java/org/springframework/security/authentication/ProviderManager.java b/core/src/main/java/org/springframework/security/authentication/ProviderManager.java index aa8b82bcd7..4769bfc191 100644 --- a/core/src/main/java/org/springframework/security/authentication/ProviderManager.java +++ b/core/src/main/java/org/springframework/security/authentication/ProviderManager.java @@ -22,6 +22,7 @@ import java.util.List; 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.context.MessageSource; @@ -97,7 +98,7 @@ public class ProviderManager implements AuthenticationManager, MessageSourceAwar protected MessageSourceAccessor messages = SpringSecurityMessageSource.getAccessor(); - private AuthenticationManager parent; + private @Nullable AuthenticationManager parent; private boolean eraseCredentialsAfterAuthentication = true; @@ -122,7 +123,7 @@ public class ProviderManager implements AuthenticationManager, MessageSourceAwar * @param providers the {@link AuthenticationProvider}s to use * @param parent a parent {@link AuthenticationManager} to fall back to */ - public ProviderManager(List providers, AuthenticationManager parent) { + public ProviderManager(List providers, @Nullable AuthenticationManager parent) { Assert.notNull(providers, "providers list cannot be null"); this.providers = providers; this.parent = parent; diff --git a/core/src/main/java/org/springframework/security/authentication/RememberMeAuthenticationProvider.java b/core/src/main/java/org/springframework/security/authentication/RememberMeAuthenticationProvider.java index ed9df33a28..75588a967a 100644 --- a/core/src/main/java/org/springframework/security/authentication/RememberMeAuthenticationProvider.java +++ b/core/src/main/java/org/springframework/security/authentication/RememberMeAuthenticationProvider.java @@ -16,6 +16,8 @@ package org.springframework.security.authentication; +import org.jspecify.annotations.Nullable; + import org.springframework.beans.factory.InitializingBean; import org.springframework.context.MessageSource; import org.springframework.context.MessageSourceAware; @@ -49,7 +51,7 @@ public class RememberMeAuthenticationProvider implements AuthenticationProvider, } @Override - public Authentication authenticate(Authentication authentication) throws AuthenticationException { + public @Nullable Authentication authenticate(Authentication authentication) throws AuthenticationException { if (!supports(authentication.getClass())) { return null; } diff --git a/core/src/main/java/org/springframework/security/authentication/UsernamePasswordAuthenticationToken.java b/core/src/main/java/org/springframework/security/authentication/UsernamePasswordAuthenticationToken.java index be796d04a4..7c8af99bcd 100644 --- a/core/src/main/java/org/springframework/security/authentication/UsernamePasswordAuthenticationToken.java +++ b/core/src/main/java/org/springframework/security/authentication/UsernamePasswordAuthenticationToken.java @@ -18,6 +18,8 @@ package org.springframework.security.authentication; import java.util.Collection; +import org.jspecify.annotations.Nullable; + import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.SpringSecurityCoreVersion; import org.springframework.util.Assert; @@ -40,7 +42,7 @@ public class UsernamePasswordAuthenticationToken extends AbstractAuthenticationT private final Object principal; - private Object credentials; + private @Nullable Object credentials; /** * This constructor can be safely used by any code that wishes to create a @@ -48,7 +50,7 @@ public class UsernamePasswordAuthenticationToken extends AbstractAuthenticationT * will return false. * */ - public UsernamePasswordAuthenticationToken(Object principal, Object credentials) { + public UsernamePasswordAuthenticationToken(Object principal, @Nullable Object credentials) { super(null); this.principal = principal; this.credentials = credentials; @@ -64,7 +66,7 @@ public class UsernamePasswordAuthenticationToken extends AbstractAuthenticationT * @param credentials * @param authorities */ - public UsernamePasswordAuthenticationToken(Object principal, Object credentials, + public UsernamePasswordAuthenticationToken(Object principal, @Nullable Object credentials, Collection authorities) { super(authorities); this.principal = principal; @@ -81,7 +83,7 @@ public class UsernamePasswordAuthenticationToken extends AbstractAuthenticationT * * @since 5.7 */ - public static UsernamePasswordAuthenticationToken unauthenticated(Object principal, Object credentials) { + public static UsernamePasswordAuthenticationToken unauthenticated(Object principal, @Nullable Object credentials) { return new UsernamePasswordAuthenticationToken(principal, credentials); } @@ -94,13 +96,13 @@ public class UsernamePasswordAuthenticationToken extends AbstractAuthenticationT * * @since 5.7 */ - public static UsernamePasswordAuthenticationToken authenticated(Object principal, Object credentials, + public static UsernamePasswordAuthenticationToken authenticated(Object principal, @Nullable Object credentials, Collection authorities) { return new UsernamePasswordAuthenticationToken(principal, credentials, authorities); } @Override - public Object getCredentials() { + public @Nullable Object getCredentials() { return this.credentials; } diff --git a/core/src/main/java/org/springframework/security/authentication/dao/DaoAuthenticationProvider.java b/core/src/main/java/org/springframework/security/authentication/dao/DaoAuthenticationProvider.java index 099b1ab477..c599a1504f 100644 --- a/core/src/main/java/org/springframework/security/authentication/dao/DaoAuthenticationProvider.java +++ b/core/src/main/java/org/springframework/security/authentication/dao/DaoAuthenticationProvider.java @@ -18,6 +18,8 @@ package org.springframework.security.authentication.dao; import java.util.function.Supplier; +import org.jspecify.annotations.Nullable; + import org.springframework.security.authentication.AuthenticationProvider; import org.springframework.security.authentication.BadCredentialsException; import org.springframework.security.authentication.InternalAuthenticationServiceException; @@ -60,15 +62,16 @@ public class DaoAuthenticationProvider extends AbstractUserDetailsAuthentication * {@link PasswordEncoder} implementations will short circuit if the password is not * in a valid format. */ - private volatile String userNotFoundEncodedPassword; + private volatile @Nullable String userNotFoundEncodedPassword; - private UserDetailsService userDetailsService; + private final UserDetailsService userDetailsService; - private UserDetailsPasswordService userDetailsPasswordService; + private UserDetailsPasswordService userDetailsPasswordService = UserDetailsPasswordService.NOOP; - private CompromisedPasswordChecker compromisedPasswordChecker; + private @Nullable CompromisedPasswordChecker compromisedPasswordChecker; public DaoAuthenticationProvider(UserDetailsService userDetailsService) { + Assert.notNull(userDetailsService, "userDetailsService cannot be null"); this.userDetailsService = userDetailsService; } @@ -120,14 +123,16 @@ public class DaoAuthenticationProvider extends AbstractUserDetailsAuthentication @Override protected Authentication createSuccessAuthentication(Object principal, Authentication authentication, UserDetails user) { + Assert.notNull(authentication.getCredentials(), "Authentication.getCredentials() cannot be null"); String presentedPassword = authentication.getCredentials().toString(); boolean isPasswordCompromised = this.compromisedPasswordChecker != null && this.compromisedPasswordChecker.check(presentedPassword).isCompromised(); if (isPasswordCompromised) { throw new CompromisedPasswordException("The provided password is compromised, please change your password"); } - boolean upgradeEncoding = this.userDetailsPasswordService != null - && this.passwordEncoder.get().upgradeEncoding(user.getPassword()); + String existingEncodedPassword = user.getPassword(); + boolean upgradeEncoding = existingEncodedPassword != null && this.userDetailsPasswordService != null + && this.passwordEncoder.get().upgradeEncoding(existingEncodedPassword); if (upgradeEncoding) { String newPassword = this.passwordEncoder.get().encode(presentedPassword); user = this.userDetailsPasswordService.updatePassword(user, newPassword); @@ -143,6 +148,7 @@ public class DaoAuthenticationProvider extends AbstractUserDetailsAuthentication private void mitigateAgainstTimingAttack(UsernamePasswordAuthenticationToken authentication) { if (authentication.getCredentials() != null) { + Assert.notNull(this.userNotFoundEncodedPassword, "userNotFoundEncodedPassword cannot be null"); String presentedPassword = authentication.getCredentials().toString(); this.passwordEncoder.get().matches(presentedPassword, this.userNotFoundEncodedPassword); } @@ -170,6 +176,7 @@ public class DaoAuthenticationProvider extends AbstractUserDetailsAuthentication } public void setUserDetailsPasswordService(UserDetailsPasswordService userDetailsPasswordService) { + Assert.notNull(userDetailsPasswordService, "userDetailsPasswordService cannot be null"); this.userDetailsPasswordService = userDetailsPasswordService; } @@ -180,6 +187,7 @@ public class DaoAuthenticationProvider extends AbstractUserDetailsAuthentication * @since 6.3 */ public void setCompromisedPasswordChecker(CompromisedPasswordChecker compromisedPasswordChecker) { + Assert.notNull(compromisedPasswordChecker, "compromisedPasswordChecker cannot be null"); this.compromisedPasswordChecker = compromisedPasswordChecker; } diff --git a/core/src/main/java/org/springframework/security/authentication/dao/package-info.java b/core/src/main/java/org/springframework/security/authentication/dao/package-info.java index b5668f899e..9ad4a467fc 100644 --- a/core/src/main/java/org/springframework/security/authentication/dao/package-info.java +++ b/core/src/main/java/org/springframework/security/authentication/dao/package-info.java @@ -17,4 +17,7 @@ /** * An {@code AuthenticationProvider} which relies upon a data access object. */ +@NullMarked package org.springframework.security.authentication.dao; + +import org.jspecify.annotations.NullMarked; diff --git a/core/src/main/java/org/springframework/security/authentication/event/package-info.java b/core/src/main/java/org/springframework/security/authentication/event/package-info.java index b55823abf4..27520e2b04 100644 --- a/core/src/main/java/org/springframework/security/authentication/event/package-info.java +++ b/core/src/main/java/org/springframework/security/authentication/event/package-info.java @@ -22,4 +22,7 @@ * context. These events are received by all registered Spring * ApplicationListeners. */ +@NullMarked package org.springframework.security.authentication.event; + +import org.jspecify.annotations.NullMarked; diff --git a/core/src/main/java/org/springframework/security/authentication/jaas/AbstractJaasAuthenticationProvider.java b/core/src/main/java/org/springframework/security/authentication/jaas/AbstractJaasAuthenticationProvider.java index 05aaeb585d..1678b5d34b 100644 --- a/core/src/main/java/org/springframework/security/authentication/jaas/AbstractJaasAuthenticationProvider.java +++ b/core/src/main/java/org/springframework/security/authentication/jaas/AbstractJaasAuthenticationProvider.java @@ -31,6 +31,7 @@ import javax.security.auth.login.LoginException; 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.context.ApplicationEventPublisher; @@ -119,11 +120,12 @@ import org.springframework.util.ObjectUtils; public abstract class AbstractJaasAuthenticationProvider implements AuthenticationProvider, ApplicationEventPublisherAware, InitializingBean, ApplicationListener { - private ApplicationEventPublisher applicationEventPublisher; + private ApplicationEventPublisher applicationEventPublisher = (event) -> { + }; - private AuthorityGranter[] authorityGranters; + private AuthorityGranter[] authorityGranters = new AuthorityGranter[0]; - private JaasAuthenticationCallbackHandler[] callbackHandlers; + private JaasAuthenticationCallbackHandler[] callbackHandlers = new JaasAuthenticationCallbackHandler[0]; protected final Log log = LogFactory.getLog(getClass()); @@ -159,7 +161,7 @@ public abstract class AbstractJaasAuthenticationProvider implements Authenticati * loginContext.login() method fail. */ @Override - public Authentication authenticate(Authentication auth) throws AuthenticationException { + public @Nullable Authentication authenticate(Authentication auth) throws AuthenticationException { if (!(auth instanceof UsernamePasswordAuthenticationToken request)) { return null; } @@ -303,6 +305,7 @@ public abstract class AbstractJaasAuthenticationProvider implements Authenticati * @see JaasAuthenticationProvider */ public void setAuthorityGranters(AuthorityGranter[] authorityGranters) { + Assert.notNull(authorityGranters, "authorityGranters cannot be null"); this.authorityGranters = authorityGranters; } @@ -323,6 +326,7 @@ public abstract class AbstractJaasAuthenticationProvider implements Authenticati * @param callbackHandlers Array of JAASAuthenticationCallbackHandlers */ public void setCallbackHandlers(JaasAuthenticationCallbackHandler[] callbackHandlers) { + Assert.notNull(callbackHandlers, "callbackHandlers cannot be null"); this.callbackHandlers = callbackHandlers; } @@ -354,6 +358,7 @@ public abstract class AbstractJaasAuthenticationProvider implements Authenticati @Override public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) { + Assert.notNull(applicationEventPublisher, "applicationEventPublisher cannot be null"); this.applicationEventPublisher = applicationEventPublisher; } diff --git a/core/src/main/java/org/springframework/security/authentication/jaas/DefaultJaasAuthenticationProvider.java b/core/src/main/java/org/springframework/security/authentication/jaas/DefaultJaasAuthenticationProvider.java index 924f570831..f97c4fae93 100644 --- a/core/src/main/java/org/springframework/security/authentication/jaas/DefaultJaasAuthenticationProvider.java +++ b/core/src/main/java/org/springframework/security/authentication/jaas/DefaultJaasAuthenticationProvider.java @@ -87,6 +87,7 @@ import org.springframework.util.Assert; */ public class DefaultJaasAuthenticationProvider extends AbstractJaasAuthenticationProvider { + @SuppressWarnings("NullAway.Init") private Configuration configuration; @Override diff --git a/core/src/main/java/org/springframework/security/authentication/jaas/JaasAuthenticationProvider.java b/core/src/main/java/org/springframework/security/authentication/jaas/JaasAuthenticationProvider.java index 6e3d2cba25..e32152b0ef 100644 --- a/core/src/main/java/org/springframework/security/authentication/jaas/JaasAuthenticationProvider.java +++ b/core/src/main/java/org/springframework/security/authentication/jaas/JaasAuthenticationProvider.java @@ -143,6 +143,7 @@ public class JaasAuthenticationProvider extends AbstractJaasAuthenticationProvid // exists for passivity protected static final Log log = LogFactory.getLog(JaasAuthenticationProvider.class); + @SuppressWarnings("NullAway.Init") private Resource loginConfig; private boolean refreshConfigurationOnStartup = true; diff --git a/core/src/main/java/org/springframework/security/authentication/jaas/JaasAuthenticationToken.java b/core/src/main/java/org/springframework/security/authentication/jaas/JaasAuthenticationToken.java index 410f7e5f06..57f43c2aac 100644 --- a/core/src/main/java/org/springframework/security/authentication/jaas/JaasAuthenticationToken.java +++ b/core/src/main/java/org/springframework/security/authentication/jaas/JaasAuthenticationToken.java @@ -20,6 +20,8 @@ import java.util.List; import javax.security.auth.login.LoginContext; +import org.jspecify.annotations.Nullable; + import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.SpringSecurityCoreVersion; @@ -36,12 +38,12 @@ public class JaasAuthenticationToken extends UsernamePasswordAuthenticationToken private final transient LoginContext loginContext; - public JaasAuthenticationToken(Object principal, Object credentials, LoginContext loginContext) { + public JaasAuthenticationToken(Object principal, @Nullable Object credentials, LoginContext loginContext) { super(principal, credentials); this.loginContext = loginContext; } - public JaasAuthenticationToken(Object principal, Object credentials, List authorities, + public JaasAuthenticationToken(Object principal, @Nullable Object credentials, List authorities, LoginContext loginContext) { super(principal, credentials, authorities); this.loginContext = loginContext; diff --git a/core/src/main/java/org/springframework/security/authentication/jaas/JaasNameCallbackHandler.java b/core/src/main/java/org/springframework/security/authentication/jaas/JaasNameCallbackHandler.java index 1f7881283f..44e4ee3830 100644 --- a/core/src/main/java/org/springframework/security/authentication/jaas/JaasNameCallbackHandler.java +++ b/core/src/main/java/org/springframework/security/authentication/jaas/JaasNameCallbackHandler.java @@ -21,6 +21,7 @@ import javax.security.auth.callback.NameCallback; import org.springframework.security.core.Authentication; import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.util.Assert; /** * The most basic Callbacks to be handled when using a LoginContext from JAAS, are the @@ -55,6 +56,7 @@ public class JaasNameCallbackHandler implements JaasAuthenticationCallbackHandle if (principal instanceof UserDetails) { return ((UserDetails) principal).getUsername(); } + Assert.notNull(principal, "principal cannot be null"); return principal.toString(); } diff --git a/core/src/main/java/org/springframework/security/authentication/jaas/JaasPasswordCallbackHandler.java b/core/src/main/java/org/springframework/security/authentication/jaas/JaasPasswordCallbackHandler.java index 8b43440052..325c23f97c 100644 --- a/core/src/main/java/org/springframework/security/authentication/jaas/JaasPasswordCallbackHandler.java +++ b/core/src/main/java/org/springframework/security/authentication/jaas/JaasPasswordCallbackHandler.java @@ -47,7 +47,10 @@ public class JaasPasswordCallbackHandler implements JaasAuthenticationCallbackHa @Override public void handle(Callback callback, Authentication auth) { if (callback instanceof PasswordCallback) { - ((PasswordCallback) callback).setPassword(auth.getCredentials().toString().toCharArray()); + Object credentials = auth.getCredentials(); + if (credentials != null) { + ((PasswordCallback) callback).setPassword(credentials.toString().toCharArray()); + } } } diff --git a/core/src/main/java/org/springframework/security/authentication/jaas/SecurityContextLoginModule.java b/core/src/main/java/org/springframework/security/authentication/jaas/SecurityContextLoginModule.java index ca88f0aacf..e46d1e323b 100644 --- a/core/src/main/java/org/springframework/security/authentication/jaas/SecurityContextLoginModule.java +++ b/core/src/main/java/org/springframework/security/authentication/jaas/SecurityContextLoginModule.java @@ -25,6 +25,7 @@ import javax.security.auth.spi.LoginModule; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.Nullable; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; @@ -60,9 +61,9 @@ public class SecurityContextLoginModule implements LoginModule { private SecurityContextHolderStrategy securityContextHolderStrategy = SecurityContextHolder .getContextHolderStrategy(); - private Authentication authen; + private @Nullable Authentication authen; - private Subject subject; + private @Nullable Subject subject; private boolean ignoreMissingAuthentication = false; @@ -92,6 +93,7 @@ public class SecurityContextLoginModule implements LoginModule { if (this.authen == null) { return false; } + Assert.notNull(this.subject, "subject cannot be null"); this.subject.getPrincipals().add(this.authen); return true; } @@ -107,11 +109,11 @@ public class SecurityContextLoginModule implements LoginModule { this.securityContextHolderStrategy = securityContextHolderStrategy; } - Authentication getAuthentication() { + @Nullable Authentication getAuthentication() { return this.authen; } - Subject getSubject() { + @Nullable Subject getSubject() { return this.subject; } @@ -165,6 +167,7 @@ public class SecurityContextLoginModule implements LoginModule { if (this.authen == null) { return false; } + Assert.notNull(this.subject, "subject cannot be null"); this.subject.getPrincipals().remove(this.authen); this.authen = null; return true; diff --git a/core/src/main/java/org/springframework/security/authentication/jaas/event/package-info.java b/core/src/main/java/org/springframework/security/authentication/jaas/event/package-info.java index 802ac1c9d1..80b6d5b890 100644 --- a/core/src/main/java/org/springframework/security/authentication/jaas/event/package-info.java +++ b/core/src/main/java/org/springframework/security/authentication/jaas/event/package-info.java @@ -18,4 +18,7 @@ * JAAS authentication events which can be published to the Spring application context by * the JAAS authentication provider. */ +@NullMarked package org.springframework.security.authentication.jaas.event; + +import org.jspecify.annotations.NullMarked; diff --git a/core/src/main/java/org/springframework/security/authentication/jaas/memory/InMemoryConfiguration.java b/core/src/main/java/org/springframework/security/authentication/jaas/memory/InMemoryConfiguration.java index 49a6567d30..3e9654a411 100644 --- a/core/src/main/java/org/springframework/security/authentication/jaas/memory/InMemoryConfiguration.java +++ b/core/src/main/java/org/springframework/security/authentication/jaas/memory/InMemoryConfiguration.java @@ -22,6 +22,8 @@ import java.util.Map; import javax.security.auth.login.AppConfigurationEntry; import javax.security.auth.login.Configuration; +import org.jspecify.annotations.Nullable; + import org.springframework.util.Assert; /** @@ -37,7 +39,7 @@ import org.springframework.util.Assert; */ public class InMemoryConfiguration extends Configuration { - private final AppConfigurationEntry[] defaultConfiguration; + private final AppConfigurationEntry @Nullable [] defaultConfiguration; private final Map mappedConfigurations; @@ -71,14 +73,14 @@ public class InMemoryConfiguration extends Configuration { * {@link #getAppConfigurationEntry(String)}. Can be null. */ public InMemoryConfiguration(Map mappedConfigurations, - AppConfigurationEntry[] defaultConfiguration) { + AppConfigurationEntry @Nullable [] defaultConfiguration) { Assert.notNull(mappedConfigurations, "mappedConfigurations cannot be null."); this.mappedConfigurations = mappedConfigurations; this.defaultConfiguration = defaultConfiguration; } @Override - public AppConfigurationEntry[] getAppConfigurationEntry(String name) { + public AppConfigurationEntry @Nullable [] getAppConfigurationEntry(String name) { AppConfigurationEntry[] mappedResult = this.mappedConfigurations.get(name); return (mappedResult != null) ? mappedResult : this.defaultConfiguration; } diff --git a/core/src/main/java/org/springframework/security/authentication/jaas/memory/package-info.java b/core/src/main/java/org/springframework/security/authentication/jaas/memory/package-info.java index 4e0476288b..2faf79c097 100644 --- a/core/src/main/java/org/springframework/security/authentication/jaas/memory/package-info.java +++ b/core/src/main/java/org/springframework/security/authentication/jaas/memory/package-info.java @@ -17,4 +17,7 @@ /** * An in memory JAAS implementation. */ +@NullMarked package org.springframework.security.authentication.jaas.memory; + +import org.jspecify.annotations.NullMarked; diff --git a/core/src/main/java/org/springframework/security/authentication/jaas/package-info.java b/core/src/main/java/org/springframework/security/authentication/jaas/package-info.java index a5bb8e318d..bed8b9278b 100644 --- a/core/src/main/java/org/springframework/security/authentication/jaas/package-info.java +++ b/core/src/main/java/org/springframework/security/authentication/jaas/package-info.java @@ -17,4 +17,7 @@ /** * An authentication provider for JAAS. */ +@NullMarked package org.springframework.security.authentication.jaas; + +import org.jspecify.annotations.NullMarked; diff --git a/core/src/main/java/org/springframework/security/authentication/ott/InMemoryOneTimeTokenService.java b/core/src/main/java/org/springframework/security/authentication/ott/InMemoryOneTimeTokenService.java index 0d67961794..4940ec7930 100644 --- a/core/src/main/java/org/springframework/security/authentication/ott/InMemoryOneTimeTokenService.java +++ b/core/src/main/java/org/springframework/security/authentication/ott/InMemoryOneTimeTokenService.java @@ -22,7 +22,8 @@ import java.util.Map; import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; -import org.springframework.lang.NonNull; +import org.jspecify.annotations.Nullable; + import org.springframework.util.Assert; /** @@ -41,7 +42,6 @@ public final class InMemoryOneTimeTokenService implements OneTimeTokenService { private Clock clock = Clock.systemUTC(); @Override - @NonNull public OneTimeToken generate(GenerateOneTimeTokenRequest request) { String token = UUID.randomUUID().toString(); Instant expiresAt = this.clock.instant().plus(request.getExpiresIn()); @@ -52,7 +52,7 @@ public final class InMemoryOneTimeTokenService implements OneTimeTokenService { } @Override - public OneTimeToken consume(OneTimeTokenAuthenticationToken authenticationToken) { + public @Nullable OneTimeToken consume(OneTimeTokenAuthenticationToken authenticationToken) { OneTimeToken ott = this.oneTimeTokenByToken.remove(authenticationToken.getTokenValue()); if (ott == null || isExpired(ott)) { return null; diff --git a/core/src/main/java/org/springframework/security/authentication/ott/JdbcOneTimeTokenService.java b/core/src/main/java/org/springframework/security/authentication/ott/JdbcOneTimeTokenService.java index a58665bd1e..2d20a3b1e3 100644 --- a/core/src/main/java/org/springframework/security/authentication/ott/JdbcOneTimeTokenService.java +++ b/core/src/main/java/org/springframework/security/authentication/ott/JdbcOneTimeTokenService.java @@ -29,6 +29,7 @@ import java.util.function.Function; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.Nullable; import org.springframework.beans.factory.DisposableBean; import org.springframework.beans.factory.InitializingBean; @@ -68,7 +69,7 @@ public final class JdbcOneTimeTokenService implements OneTimeTokenService, Dispo private Clock clock = Clock.systemUTC(); - private ThreadPoolTaskScheduler taskScheduler; + private @Nullable ThreadPoolTaskScheduler taskScheduler; private static final String DEFAULT_CLEANUP_CRON = "@hourly"; @@ -144,7 +145,7 @@ public final class JdbcOneTimeTokenService implements OneTimeTokenService, Dispo } @Override - public OneTimeToken consume(OneTimeTokenAuthenticationToken authenticationToken) { + public @Nullable OneTimeToken consume(OneTimeTokenAuthenticationToken authenticationToken) { Assert.notNull(authenticationToken, "authenticationToken cannot be null"); List tokens = selectOneTimeToken(authenticationToken); @@ -177,7 +178,7 @@ public final class JdbcOneTimeTokenService implements OneTimeTokenService, Dispo this.jdbcOperations.update(DELETE_ONE_TIME_TOKEN_SQL, pss); } - private ThreadPoolTaskScheduler createTaskScheduler(String cleanupCron) { + private @Nullable ThreadPoolTaskScheduler createTaskScheduler(String cleanupCron) { if (cleanupCron == null) { return null; } @@ -200,7 +201,9 @@ public final class JdbcOneTimeTokenService implements OneTimeTokenService, Dispo @Override public void afterPropertiesSet() throws Exception { - this.taskScheduler.afterPropertiesSet(); + if (this.taskScheduler != null) { + this.taskScheduler.afterPropertiesSet(); + } } @Override diff --git a/core/src/main/java/org/springframework/security/authentication/ott/OneTimeTokenAuthenticationToken.java b/core/src/main/java/org/springframework/security/authentication/ott/OneTimeTokenAuthenticationToken.java index 3bceb22be3..626d70e6a2 100644 --- a/core/src/main/java/org/springframework/security/authentication/ott/OneTimeTokenAuthenticationToken.java +++ b/core/src/main/java/org/springframework/security/authentication/ott/OneTimeTokenAuthenticationToken.java @@ -20,6 +20,8 @@ import java.io.Serial; import java.util.Collection; import java.util.Collections; +import org.jspecify.annotations.Nullable; + import org.springframework.security.authentication.AbstractAuthenticationToken; import org.springframework.security.core.GrantedAuthority; @@ -34,11 +36,11 @@ public class OneTimeTokenAuthenticationToken extends AbstractAuthenticationToken @Serial private static final long serialVersionUID = -8691636031126328365L; - private final Object principal; + private @Nullable final Object principal; - private String tokenValue; + private @Nullable String tokenValue; - public OneTimeTokenAuthenticationToken(Object principal, String tokenValue) { + public OneTimeTokenAuthenticationToken(@Nullable Object principal, String tokenValue) { super(Collections.emptyList()); this.tokenValue = tokenValue; this.principal = principal; @@ -88,17 +90,17 @@ public class OneTimeTokenAuthenticationToken extends AbstractAuthenticationToken * Returns the one-time token value * @return */ - public String getTokenValue() { + public @Nullable String getTokenValue() { return this.tokenValue; } @Override - public Object getCredentials() { + public @Nullable Object getCredentials() { return this.tokenValue; } @Override - public Object getPrincipal() { + public @Nullable Object getPrincipal() { return this.principal; } diff --git a/core/src/main/java/org/springframework/security/authentication/ott/OneTimeTokenService.java b/core/src/main/java/org/springframework/security/authentication/ott/OneTimeTokenService.java index e8584a8fb3..4f7bca626a 100644 --- a/core/src/main/java/org/springframework/security/authentication/ott/OneTimeTokenService.java +++ b/core/src/main/java/org/springframework/security/authentication/ott/OneTimeTokenService.java @@ -16,8 +16,7 @@ package org.springframework.security.authentication.ott; -import org.springframework.lang.NonNull; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * Interface for generating and consuming one-time tokens. @@ -33,7 +32,6 @@ public interface OneTimeTokenService { * generate the token * @return the generated {@link OneTimeToken}, never {@code null}. */ - @NonNull OneTimeToken generate(GenerateOneTimeTokenRequest request); /** @@ -42,7 +40,6 @@ public interface OneTimeTokenService { * value to be consumed * @return the consumed {@link OneTimeToken} or {@code null} if the token is invalid */ - @Nullable - OneTimeToken consume(OneTimeTokenAuthenticationToken authenticationToken); + @Nullable OneTimeToken consume(OneTimeTokenAuthenticationToken authenticationToken); } diff --git a/core/src/main/java/org/springframework/security/authentication/ott/package-info.java b/core/src/main/java/org/springframework/security/authentication/ott/package-info.java new file mode 100644 index 0000000000..c9638c5878 --- /dev/null +++ b/core/src/main/java/org/springframework/security/authentication/ott/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright 2002-2025 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.authentication.ott; + +import org.jspecify.annotations.NullMarked; diff --git a/core/src/main/java/org/springframework/security/authentication/ott/reactive/package-info.java b/core/src/main/java/org/springframework/security/authentication/ott/reactive/package-info.java new file mode 100644 index 0000000000..b47ab15651 --- /dev/null +++ b/core/src/main/java/org/springframework/security/authentication/ott/reactive/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright 2002-2025 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.authentication.ott.reactive; + +import org.jspecify.annotations.NullMarked; diff --git a/core/src/main/java/org/springframework/security/authentication/package-info.java b/core/src/main/java/org/springframework/security/authentication/package-info.java index b133c48be4..3548e24df7 100644 --- a/core/src/main/java/org/springframework/security/authentication/package-info.java +++ b/core/src/main/java/org/springframework/security/authentication/package-info.java @@ -26,4 +26,7 @@ * {@link org.springframework.security.authentication.AuthenticationProvider * AuthenticationProvider}s to which it delegates authentication requests. */ +@NullMarked package org.springframework.security.authentication; + +import org.jspecify.annotations.NullMarked; diff --git a/core/src/main/java/org/springframework/security/authentication/password/CompromisedPasswordChecker.java b/core/src/main/java/org/springframework/security/authentication/password/CompromisedPasswordChecker.java index c77af2b603..b4644205c4 100644 --- a/core/src/main/java/org/springframework/security/authentication/password/CompromisedPasswordChecker.java +++ b/core/src/main/java/org/springframework/security/authentication/password/CompromisedPasswordChecker.java @@ -16,7 +16,7 @@ package org.springframework.security.authentication.password; -import org.springframework.lang.NonNull; +import org.jspecify.annotations.Nullable; /** * An API for checking if a password has been compromised. @@ -27,11 +27,12 @@ import org.springframework.lang.NonNull; public interface CompromisedPasswordChecker { /** - * Check whether the password is compromised + * Check whether the password is compromised. If password is null, then the return + * value must be false for {@link CompromisedPasswordDecision#isCompromised()} since a + * null password represents no password (e.g. the user leverages Passkeys instead). * @param password the password to check * @return a non-null {@link CompromisedPasswordDecision} */ - @NonNull - CompromisedPasswordDecision check(String password); + CompromisedPasswordDecision check(@Nullable String password); } diff --git a/core/src/main/java/org/springframework/security/authentication/password/ReactiveCompromisedPasswordChecker.java b/core/src/main/java/org/springframework/security/authentication/password/ReactiveCompromisedPasswordChecker.java index 7a93de7682..28537486e4 100644 --- a/core/src/main/java/org/springframework/security/authentication/password/ReactiveCompromisedPasswordChecker.java +++ b/core/src/main/java/org/springframework/security/authentication/password/ReactiveCompromisedPasswordChecker.java @@ -16,6 +16,7 @@ package org.springframework.security.authentication.password; +import org.jspecify.annotations.Nullable; import reactor.core.publisher.Mono; /** @@ -27,10 +28,12 @@ import reactor.core.publisher.Mono; public interface ReactiveCompromisedPasswordChecker { /** - * Check whether the password is compromised + * Check whether the password is compromised. If password is null, then the return + * value must be false for {@link CompromisedPasswordDecision#isCompromised()} since a + * null password represents no password (e.g. the user leverages Passkeys instead). * @param password the password to check * @return a {@link Mono} containing the {@link CompromisedPasswordDecision} */ - Mono check(String password); + Mono check(@Nullable String password); } diff --git a/core/src/main/java/org/springframework/security/authentication/password/package-info.java b/core/src/main/java/org/springframework/security/authentication/password/package-info.java new file mode 100644 index 0000000000..4ae6afe38e --- /dev/null +++ b/core/src/main/java/org/springframework/security/authentication/password/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright 2002-2025 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.authentication.password; + +import org.jspecify.annotations.NullMarked; diff --git a/core/src/main/java/org/springframework/security/authorization/AuthorizationManager.java b/core/src/main/java/org/springframework/security/authorization/AuthorizationManager.java index 4fdb5ab260..88eae7e5cd 100644 --- a/core/src/main/java/org/springframework/security/authorization/AuthorizationManager.java +++ b/core/src/main/java/org/springframework/security/authorization/AuthorizationManager.java @@ -18,7 +18,8 @@ package org.springframework.security.authorization; import java.util.function.Supplier; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; + import org.springframework.security.access.AccessDeniedException; import org.springframework.security.core.Authentication; @@ -30,7 +31,7 @@ import org.springframework.security.core.Authentication; * @author Evgeniy Cheban */ @FunctionalInterface -public interface AuthorizationManager { +public interface AuthorizationManager<@Nullable T> { /** * Determines if access should be granted for a specific authentication and object. @@ -53,7 +54,6 @@ public interface AuthorizationManager { * @return an {@link AuthorizationResult} * @since 6.4 */ - @Nullable - AuthorizationResult authorize(Supplier authentication, T object); + @Nullable AuthorizationResult authorize(Supplier authentication, T object); } diff --git a/core/src/main/java/org/springframework/security/authorization/AuthorizationObservationContext.java b/core/src/main/java/org/springframework/security/authorization/AuthorizationObservationContext.java index 41dbc03a84..8166173032 100644 --- a/core/src/main/java/org/springframework/security/authorization/AuthorizationObservationContext.java +++ b/core/src/main/java/org/springframework/security/authorization/AuthorizationObservationContext.java @@ -17,6 +17,7 @@ package org.springframework.security.authorization; import io.micrometer.observation.Observation; +import org.jspecify.annotations.Nullable; import org.springframework.security.core.Authentication; import org.springframework.util.Assert; @@ -29,11 +30,13 @@ import org.springframework.util.Assert; */ public class AuthorizationObservationContext extends Observation.Context { - private Authentication authentication; + // FIXME: Should we make this non-null? + private @Nullable Authentication authentication; private final T object; - private AuthorizationResult authorizationResult; + // FIXME: Should we make this non-null? + private @Nullable AuthorizationResult authorizationResult; public AuthorizationObservationContext(T object) { Assert.notNull(object, "object cannot be null"); @@ -48,7 +51,7 @@ public class AuthorizationObservationContext extends Observation.Context { * {@link Authentication}, this will return {@code null}. * @return any observed {@link Authentication}, {@code null} otherwise */ - public Authentication getAuthentication() { + public @Nullable Authentication getAuthentication() { return this.authentication; } @@ -73,7 +76,7 @@ public class AuthorizationObservationContext extends Observation.Context { * @return the observed {@link AuthorizationResult} * @since 6.4 */ - public AuthorizationResult getAuthorizationResult() { + public @Nullable AuthorizationResult getAuthorizationResult() { return this.authorizationResult; } @@ -82,7 +85,7 @@ public class AuthorizationObservationContext extends Observation.Context { * @param authorizationResult the observed {@link AuthorizationResult} * @since 6.4 */ - public void setAuthorizationResult(AuthorizationResult authorizationResult) { + public void setAuthorizationResult(@Nullable AuthorizationResult authorizationResult) { this.authorizationResult = authorizationResult; } diff --git a/core/src/main/java/org/springframework/security/authorization/AuthorizationProxyFactory.java b/core/src/main/java/org/springframework/security/authorization/AuthorizationProxyFactory.java index e6e1210e74..bb308788d0 100644 --- a/core/src/main/java/org/springframework/security/authorization/AuthorizationProxyFactory.java +++ b/core/src/main/java/org/springframework/security/authorization/AuthorizationProxyFactory.java @@ -16,6 +16,8 @@ package org.springframework.security.authorization; +import org.jspecify.annotations.Nullable; + /** * A factory for wrapping arbitrary objects in authorization-related advice * @@ -37,6 +39,6 @@ public interface AuthorizationProxyFactory { * @throws org.springframework.aop.framework.AopConfigException if a proxy cannot be * created */ - T proxy(T object); + @Nullable T proxy(@Nullable T object); } diff --git a/core/src/main/java/org/springframework/security/authorization/ObservationAuthorizationManager.java b/core/src/main/java/org/springframework/security/authorization/ObservationAuthorizationManager.java index a101ecd97a..a17f62b76f 100644 --- a/core/src/main/java/org/springframework/security/authorization/ObservationAuthorizationManager.java +++ b/core/src/main/java/org/springframework/security/authorization/ObservationAuthorizationManager.java @@ -22,6 +22,7 @@ import io.micrometer.observation.Observation; import io.micrometer.observation.ObservationConvention; import io.micrometer.observation.ObservationRegistry; import org.aopalliance.intercept.MethodInvocation; +import org.jspecify.annotations.Nullable; import org.springframework.context.MessageSource; import org.springframework.context.MessageSourceAware; @@ -62,7 +63,7 @@ public final class ObservationAuthorizationManager } @Override - public AuthorizationResult authorize(Supplier authentication, T object) { + public @Nullable AuthorizationResult authorize(Supplier authentication, T object) { AuthorizationObservationContext context = new AuthorizationObservationContext<>(object); Supplier wrapped = () -> { context.setAuthentication(authentication.get()); @@ -109,12 +110,13 @@ public final class ObservationAuthorizationManager } @Override - public Object handleDeniedInvocation(MethodInvocation methodInvocation, AuthorizationResult authorizationResult) { + public @Nullable Object handleDeniedInvocation(MethodInvocation methodInvocation, + AuthorizationResult authorizationResult) { return this.handler.handleDeniedInvocation(methodInvocation, authorizationResult); } @Override - public Object handleDeniedInvocationResult(MethodInvocationResult methodInvocationResult, + public @Nullable Object handleDeniedInvocationResult(MethodInvocationResult methodInvocationResult, AuthorizationResult authorizationResult) { return this.handler.handleDeniedInvocationResult(methodInvocationResult, authorizationResult); } diff --git a/core/src/main/java/org/springframework/security/authorization/ObservationReactiveAuthorizationManager.java b/core/src/main/java/org/springframework/security/authorization/ObservationReactiveAuthorizationManager.java index cb4ca0edb1..35bbd13e17 100644 --- a/core/src/main/java/org/springframework/security/authorization/ObservationReactiveAuthorizationManager.java +++ b/core/src/main/java/org/springframework/security/authorization/ObservationReactiveAuthorizationManager.java @@ -21,6 +21,7 @@ import io.micrometer.observation.ObservationConvention; import io.micrometer.observation.ObservationRegistry; import io.micrometer.observation.contextpropagation.ObservationThreadLocalAccessor; import org.aopalliance.intercept.MethodInvocation; +import org.jspecify.annotations.Nullable; import reactor.core.publisher.Mono; import org.springframework.security.access.AccessDeniedException; @@ -92,12 +93,13 @@ public final class ObservationReactiveAuthorizationManager } @Override - public Object handleDeniedInvocation(MethodInvocation methodInvocation, AuthorizationResult authorizationResult) { + public @Nullable Object handleDeniedInvocation(MethodInvocation methodInvocation, + AuthorizationResult authorizationResult) { return this.handler.handleDeniedInvocation(methodInvocation, authorizationResult); } @Override - public Object handleDeniedInvocationResult(MethodInvocationResult methodInvocationResult, + public @Nullable Object handleDeniedInvocationResult(MethodInvocationResult methodInvocationResult, AuthorizationResult authorizationResult) { return this.handler.handleDeniedInvocationResult(methodInvocationResult, authorizationResult); } diff --git a/core/src/main/java/org/springframework/security/authorization/ReactiveAuthorizationManager.java b/core/src/main/java/org/springframework/security/authorization/ReactiveAuthorizationManager.java index 36f4875fe9..a62e974d29 100644 --- a/core/src/main/java/org/springframework/security/authorization/ReactiveAuthorizationManager.java +++ b/core/src/main/java/org/springframework/security/authorization/ReactiveAuthorizationManager.java @@ -16,6 +16,7 @@ package org.springframework.security.authorization; +import org.jspecify.annotations.Nullable; import reactor.core.publisher.Mono; import org.springframework.security.access.AccessDeniedException; @@ -29,7 +30,7 @@ import org.springframework.security.core.Authentication; * @author Rob Winch * @since 5.0 */ -public interface ReactiveAuthorizationManager { +public interface ReactiveAuthorizationManager<@Nullable T> { /** * Determines if access should be granted for a specific authentication and object diff --git a/core/src/main/java/org/springframework/security/authorization/event/package-info.java b/core/src/main/java/org/springframework/security/authorization/event/package-info.java new file mode 100644 index 0000000000..17669a8cdf --- /dev/null +++ b/core/src/main/java/org/springframework/security/authorization/event/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright 2002-2025 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.authorization.event; + +import org.jspecify.annotations.NullMarked; diff --git a/core/src/main/java/org/springframework/security/authorization/method/AbstractAuthorizationManagerRegistry.java b/core/src/main/java/org/springframework/security/authorization/method/AbstractAuthorizationManagerRegistry.java index 5a2b4fba7f..84233e1ed5 100644 --- a/core/src/main/java/org/springframework/security/authorization/method/AbstractAuthorizationManagerRegistry.java +++ b/core/src/main/java/org/springframework/security/authorization/method/AbstractAuthorizationManagerRegistry.java @@ -21,9 +21,9 @@ import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import org.aopalliance.intercept.MethodInvocation; +import org.jspecify.annotations.Nullable; import org.springframework.core.MethodClassKey; -import org.springframework.lang.NonNull; import org.springframework.security.authorization.AuthorizationManager; /** @@ -57,7 +57,6 @@ abstract class AbstractAuthorizationManagerRegistry { * @param targetClass the target class * @return the non-null {@link AuthorizationManager} */ - @NonNull - abstract AuthorizationManager resolveManager(Method method, Class targetClass); + abstract AuthorizationManager resolveManager(Method method, @Nullable Class targetClass); } diff --git a/core/src/main/java/org/springframework/security/authorization/method/AbstractExpressionAttributeRegistry.java b/core/src/main/java/org/springframework/security/authorization/method/AbstractExpressionAttributeRegistry.java index 0b3c4614ba..f27ff35606 100644 --- a/core/src/main/java/org/springframework/security/authorization/method/AbstractExpressionAttributeRegistry.java +++ b/core/src/main/java/org/springframework/security/authorization/method/AbstractExpressionAttributeRegistry.java @@ -21,9 +21,9 @@ import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import org.aopalliance.intercept.MethodInvocation; +import org.jspecify.annotations.Nullable; import org.springframework.core.MethodClassKey; -import org.springframework.lang.NonNull; import org.springframework.security.access.expression.method.DefaultMethodSecurityExpressionHandler; import org.springframework.security.access.expression.method.MethodSecurityExpressionHandler; import org.springframework.security.core.annotation.AnnotationTemplateExpressionDefaults; @@ -59,7 +59,7 @@ abstract class AbstractExpressionAttributeRegistry targetClass) { + final T getAttribute(Method method, @Nullable Class targetClass) { MethodClassKey cacheKey = new MethodClassKey(method, targetClass); return this.cachedAttributes.computeIfAbsent(cacheKey, (k) -> resolveAttribute(method, targetClass)); } @@ -84,12 +84,11 @@ abstract class AbstractExpressionAttributeRegistry targetClass); + abstract @Nullable T resolveAttribute(Method method, @Nullable Class targetClass); - Class targetClass(Method method, Class targetClass) { + Class targetClass(Method method, @Nullable Class targetClass) { return (targetClass != null) ? targetClass : method.getDeclaringClass(); } diff --git a/core/src/main/java/org/springframework/security/authorization/method/AuthorizationAdvisorProxyFactory.java b/core/src/main/java/org/springframework/security/authorization/method/AuthorizationAdvisorProxyFactory.java index f0f3984cb7..fe2a52c19f 100644 --- a/core/src/main/java/org/springframework/security/authorization/method/AuthorizationAdvisorProxyFactory.java +++ b/core/src/main/java/org/springframework/security/authorization/method/AuthorizationAdvisorProxyFactory.java @@ -40,6 +40,7 @@ import java.util.stream.Stream; import org.aopalliance.aop.Advice; import org.aopalliance.intercept.MethodInvocation; +import org.jspecify.annotations.Nullable; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -49,7 +50,6 @@ import org.springframework.aop.framework.AopInfrastructureBean; import org.springframework.aop.framework.ProxyFactory; import org.springframework.beans.factory.SmartInitializingSingleton; import org.springframework.core.annotation.AnnotationAwareOrderComparator; -import org.springframework.lang.NonNull; import org.springframework.security.authorization.AuthorizationProxyFactory; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; @@ -172,7 +172,7 @@ public final class AuthorizationAdvisorProxyFactory implements AuthorizationProx * @return the proxied instance */ @Override - public T proxy(T target) { + public @Nullable T proxy(@Nullable T target) { if (target == null) { return null; } @@ -278,7 +278,6 @@ public final class AuthorizationAdvisorProxyFactory implements AuthorizationProx } @Override - @NonNull public Iterator iterator() { return this.advisors.iterator(); } @@ -312,7 +311,7 @@ public final class AuthorizationAdvisorProxyFactory implements AuthorizationProx * @param target the object to proxy * @return the visited (and possibly proxied) object */ - Object visit(AuthorizationAdvisorProxyFactory proxyFactory, Object target); + @Nullable Object visit(AuthorizationAdvisorProxyFactory proxyFactory, Object target); /** * The default {@link TargetVisitor}, which will proxy {@link Class} instances as @@ -361,7 +360,7 @@ public final class AuthorizationAdvisorProxyFactory implements AuthorizationProx private static final class IgnoreValueTypeVisitor implements TargetVisitor { @Override - public Object visit(AuthorizationAdvisorProxyFactory proxyFactory, Object object) { + public @Nullable Object visit(AuthorizationAdvisorProxyFactory proxyFactory, Object object) { if (ClassUtils.isSimpleValueType(object.getClass())) { return object; } @@ -375,7 +374,7 @@ public final class AuthorizationAdvisorProxyFactory implements AuthorizationProx private final AuthorizationProxyMethodInterceptor authorizationProxy = new AuthorizationProxyMethodInterceptor(); @Override - public Object visit(AuthorizationAdvisorProxyFactory proxyFactory, Object object) { + public @Nullable Object visit(AuthorizationAdvisorProxyFactory proxyFactory, Object object) { if (object instanceof Class targetClass) { if (AuthorizationProxy.class.isAssignableFrom(targetClass)) { return targetClass; @@ -400,7 +399,7 @@ public final class AuthorizationAdvisorProxyFactory implements AuthorizationProx private static final class ContainerTypeVisitor implements TargetVisitor { @Override - public Object visit(AuthorizationAdvisorProxyFactory proxyFactory, Object target) { + public @Nullable Object visit(AuthorizationAdvisorProxyFactory proxyFactory, Object target) { if (target instanceof Iterator iterator) { return proxyIterator(proxyFactory, iterator); } @@ -441,7 +440,7 @@ public final class AuthorizationAdvisorProxyFactory implements AuthorizationProx } @SuppressWarnings("unchecked") - private T proxyCast(AuthorizationProxyFactory proxyFactory, T target) { + private @Nullable T proxyCast(AuthorizationProxyFactory proxyFactory, T target) { return proxyFactory.proxy(target); } @@ -457,7 +456,7 @@ public final class AuthorizationAdvisorProxyFactory implements AuthorizationProx } @Override - public T next() { + public @Nullable T next() { return proxyCast(proxyFactory, iterator.next()); } }; @@ -579,7 +578,7 @@ public final class AuthorizationAdvisorProxyFactory implements AuthorizationProx @Override @SuppressWarnings("ReactiveStreamsUnusedPublisher") - public Object visit(AuthorizationAdvisorProxyFactory proxyFactory, Object target) { + public @Nullable Object visit(AuthorizationAdvisorProxyFactory proxyFactory, Object target) { if (target instanceof Mono mono) { return proxyMono(proxyFactory, mono); } @@ -605,7 +604,7 @@ public final class AuthorizationAdvisorProxyFactory implements AuthorizationProx "toAuthorizedTarget"); @Override - public Object invoke(MethodInvocation invocation) throws Throwable { + public @Nullable Object invoke(MethodInvocation invocation) throws Throwable { if (invocation.getMethod().equals(GET_TARGET_METHOD)) { return invocation.getThis(); } diff --git a/core/src/main/java/org/springframework/security/authorization/method/AuthorizationManagerAfterMethodInterceptor.java b/core/src/main/java/org/springframework/security/authorization/method/AuthorizationManagerAfterMethodInterceptor.java index a5604c5c15..a11048366a 100644 --- a/core/src/main/java/org/springframework/security/authorization/method/AuthorizationManagerAfterMethodInterceptor.java +++ b/core/src/main/java/org/springframework/security/authorization/method/AuthorizationManagerAfterMethodInterceptor.java @@ -23,6 +23,7 @@ import org.aopalliance.intercept.MethodInterceptor; import org.aopalliance.intercept.MethodInvocation; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.Nullable; import org.springframework.aop.Pointcut; import org.springframework.core.log.LogMessage; @@ -117,7 +118,7 @@ public final class AuthorizationManagerAfterMethodInterceptor implements Authori * @throws AccessDeniedException if access is not granted */ @Override - public Object invoke(MethodInvocation mi) throws Throwable { + public @Nullable Object invoke(MethodInvocation mi) throws Throwable { Object result; try { result = mi.proceed(); @@ -179,11 +180,13 @@ public final class AuthorizationManagerAfterMethodInterceptor implements Authori this.securityContextHolderStrategy = () -> strategy; } - private Object attemptAuthorization(MethodInvocation mi, Object result) { + private @Nullable Object attemptAuthorization(MethodInvocation mi, @Nullable Object result) { this.logger.debug(LogMessage.of(() -> "Authorizing method invocation " + mi)); MethodInvocationResult object = new MethodInvocationResult(mi, result); AuthorizationResult authorizationResult = this.authorizationManager.authorize(this::getAuthentication, object); - this.eventPublisher.publishAuthorizationEvent(this::getAuthentication, object, authorizationResult); + if (authorizationResult != null) { + this.eventPublisher.publishAuthorizationEvent(this::getAuthentication, object, authorizationResult); + } if (authorizationResult != null && !authorizationResult.isGranted()) { this.logger.debug(LogMessage.of(() -> "Failed to authorize " + mi + " with authorization manager " + this.authorizationManager + " and authorizationResult " + authorizationResult)); @@ -193,7 +196,7 @@ public final class AuthorizationManagerAfterMethodInterceptor implements Authori return result; } - private Object handlePostInvocationDenied(MethodInvocationResult mi, AuthorizationResult result) { + private @Nullable Object handlePostInvocationDenied(MethodInvocationResult mi, AuthorizationResult result) { if (this.authorizationManager instanceof MethodAuthorizationDeniedHandler deniedHandler) { return deniedHandler.handleDeniedInvocationResult(mi, result); } diff --git a/core/src/main/java/org/springframework/security/authorization/method/AuthorizationManagerAfterReactiveMethodInterceptor.java b/core/src/main/java/org/springframework/security/authorization/method/AuthorizationManagerAfterReactiveMethodInterceptor.java index 8ac5a70661..da3b6e874b 100644 --- a/core/src/main/java/org/springframework/security/authorization/method/AuthorizationManagerAfterReactiveMethodInterceptor.java +++ b/core/src/main/java/org/springframework/security/authorization/method/AuthorizationManagerAfterReactiveMethodInterceptor.java @@ -23,6 +23,7 @@ import kotlinx.coroutines.reactive.ReactiveFlowKt; import org.aopalliance.aop.Advice; import org.aopalliance.intercept.MethodInterceptor; import org.aopalliance.intercept.MethodInvocation; +import org.jspecify.annotations.Nullable; import org.reactivestreams.Publisher; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -155,14 +156,15 @@ public final class AuthorizationManagerAfterReactiveMethodInterceptor implements return (adapter != null) ? adapter.fromPublisher(mono) : mono; } - private boolean isMultiValue(Class returnType, ReactiveAdapter adapter) { + private boolean isMultiValue(Class returnType, @Nullable ReactiveAdapter adapter) { if (Flux.class.isAssignableFrom(returnType)) { return true; } return adapter != null && adapter.isMultiValue(); } - private Mono postAuthorize(Mono authentication, MethodInvocation mi, Object result) { + private Mono postAuthorize(Mono authentication, MethodInvocation mi, + @Nullable Object result) { MethodInvocationResult invocationResult = new MethodInvocationResult(mi, result); return this.authorizationManager.authorize(authentication, invocationResult) .switchIfEmpty(Mono.just(new AuthorizationDecision(false))) diff --git a/core/src/main/java/org/springframework/security/authorization/method/AuthorizationManagerBeforeMethodInterceptor.java b/core/src/main/java/org/springframework/security/authorization/method/AuthorizationManagerBeforeMethodInterceptor.java index 4e67ca9f80..32eae4b511 100644 --- a/core/src/main/java/org/springframework/security/authorization/method/AuthorizationManagerBeforeMethodInterceptor.java +++ b/core/src/main/java/org/springframework/security/authorization/method/AuthorizationManagerBeforeMethodInterceptor.java @@ -26,6 +26,7 @@ import org.aopalliance.intercept.MethodInterceptor; import org.aopalliance.intercept.MethodInvocation; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.Nullable; import org.springframework.aop.Pointcut; import org.springframework.core.log.LogMessage; @@ -192,7 +193,7 @@ public final class AuthorizationManagerBeforeMethodInterceptor implements Author * @throws AccessDeniedException if access is not granted */ @Override - public Object invoke(MethodInvocation mi) throws Throwable { + public @Nullable Object invoke(MethodInvocation mi) throws Throwable { return attemptAuthorization(mi); } @@ -244,7 +245,7 @@ public final class AuthorizationManagerBeforeMethodInterceptor implements Author this.securityContextHolderStrategy = () -> securityContextHolderStrategy; } - private Object attemptAuthorization(MethodInvocation mi) throws Throwable { + private @Nullable Object attemptAuthorization(MethodInvocation mi) throws Throwable { this.logger.debug(LogMessage.of(() -> "Authorizing method invocation " + mi)); AuthorizationResult result; try { @@ -253,7 +254,9 @@ public final class AuthorizationManagerBeforeMethodInterceptor implements Author catch (AuthorizationDeniedException denied) { return handle(mi, denied); } - this.eventPublisher.publishAuthorizationEvent(this::getAuthentication, mi, result); + if (result != null) { + this.eventPublisher.publishAuthorizationEvent(this::getAuthentication, mi, result); + } if (result != null && !result.isGranted()) { this.logger.debug(LogMessage.of(() -> "Failed to authorize " + mi + " with authorization manager " + this.authorizationManager + " and result " + result)); @@ -263,7 +266,7 @@ public final class AuthorizationManagerBeforeMethodInterceptor implements Author return proceed(mi); } - private Object proceed(MethodInvocation mi) throws Throwable { + private @Nullable Object proceed(MethodInvocation mi) throws Throwable { try { return mi.proceed(); } @@ -275,14 +278,14 @@ public final class AuthorizationManagerBeforeMethodInterceptor implements Author } } - private Object handle(MethodInvocation mi, AuthorizationDeniedException denied) { + private @Nullable Object handle(MethodInvocation mi, AuthorizationDeniedException denied) { if (this.authorizationManager instanceof MethodAuthorizationDeniedHandler handler) { return handler.handleDeniedInvocation(mi, denied); } return this.defaultHandler.handleDeniedInvocation(mi, denied); } - private Object handle(MethodInvocation mi, AuthorizationResult result) { + private @Nullable Object handle(MethodInvocation mi, AuthorizationResult result) { if (this.authorizationManager instanceof MethodAuthorizationDeniedHandler handler) { return handler.handleDeniedInvocation(mi, result); } diff --git a/core/src/main/java/org/springframework/security/authorization/method/AuthorizationManagerBeforeReactiveMethodInterceptor.java b/core/src/main/java/org/springframework/security/authorization/method/AuthorizationManagerBeforeReactiveMethodInterceptor.java index 50e1883e72..4aa094220f 100644 --- a/core/src/main/java/org/springframework/security/authorization/method/AuthorizationManagerBeforeReactiveMethodInterceptor.java +++ b/core/src/main/java/org/springframework/security/authorization/method/AuthorizationManagerBeforeReactiveMethodInterceptor.java @@ -22,6 +22,7 @@ import kotlinx.coroutines.reactive.ReactiveFlowKt; import org.aopalliance.aop.Advice; import org.aopalliance.intercept.MethodInterceptor; import org.aopalliance.intercept.MethodInvocation; +import org.jspecify.annotations.Nullable; import org.reactivestreams.Publisher; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -178,7 +179,7 @@ public final class AuthorizationManagerBeforeReactiveMethodInterceptor implement }); } - private boolean isMultiValue(Class returnType, ReactiveAdapter adapter) { + private boolean isMultiValue(Class returnType, @Nullable ReactiveAdapter adapter) { if (Flux.class.isAssignableFrom(returnType)) { return true; } diff --git a/core/src/main/java/org/springframework/security/authorization/method/AuthorizationMethodPointcuts.java b/core/src/main/java/org/springframework/security/authorization/method/AuthorizationMethodPointcuts.java index cbdba35f3a..555ee5406b 100644 --- a/core/src/main/java/org/springframework/security/authorization/method/AuthorizationMethodPointcuts.java +++ b/core/src/main/java/org/springframework/security/authorization/method/AuthorizationMethodPointcuts.java @@ -17,6 +17,7 @@ package org.springframework.security.authorization.method; import java.lang.annotation.Annotation; +import java.util.Arrays; import org.springframework.aop.Pointcut; import org.springframework.aop.support.ComposablePointcut; @@ -48,6 +49,10 @@ final class AuthorizationMethodPointcuts { pointcut.union(classOrMethod(annotation)); } } + if (pointcut == null) { + throw new IllegalStateException( + "Unable to find a pointcut for annotations " + Arrays.toString(annotations)); + } return pointcut; } diff --git a/core/src/main/java/org/springframework/security/authorization/method/AuthorizeReturnObjectMethodInterceptor.java b/core/src/main/java/org/springframework/security/authorization/method/AuthorizeReturnObjectMethodInterceptor.java index 5a69fa11f1..68f82c3c0e 100644 --- a/core/src/main/java/org/springframework/security/authorization/method/AuthorizeReturnObjectMethodInterceptor.java +++ b/core/src/main/java/org/springframework/security/authorization/method/AuthorizeReturnObjectMethodInterceptor.java @@ -21,6 +21,7 @@ import java.util.function.Predicate; import org.aopalliance.aop.Advice; import org.aopalliance.intercept.MethodInvocation; +import org.jspecify.annotations.Nullable; import org.springframework.aop.Pointcut; import org.springframework.aop.support.Pointcuts; @@ -39,7 +40,8 @@ import org.springframework.util.ClassUtils; */ public final class AuthorizeReturnObjectMethodInterceptor implements AuthorizationAdvisor { - private AuthorizationProxyFactory authorizationProxyFactory; + @SuppressWarnings("NullAway.Init") + private @Nullable AuthorizationProxyFactory authorizationProxyFactory; private Pointcut pointcut = Pointcuts.intersection( new MethodReturnTypePointcut(Predicate.not(ClassUtils::isVoidType)), @@ -66,7 +68,7 @@ public final class AuthorizeReturnObjectMethodInterceptor implements Authorizati } @Override - public Object invoke(MethodInvocation mi) throws Throwable { + public @Nullable Object invoke(MethodInvocation mi) throws Throwable { Object result = mi.proceed(); if (result == null) { return null; diff --git a/core/src/main/java/org/springframework/security/authorization/method/ExpressionAttribute.java b/core/src/main/java/org/springframework/security/authorization/method/ExpressionAttribute.java index e6cd6aa93f..99be469090 100644 --- a/core/src/main/java/org/springframework/security/authorization/method/ExpressionAttribute.java +++ b/core/src/main/java/org/springframework/security/authorization/method/ExpressionAttribute.java @@ -26,11 +26,6 @@ import org.springframework.expression.Expression; */ class ExpressionAttribute { - /** - * Represents an empty attribute with null {@link Expression}. - */ - static final ExpressionAttribute NULL_ATTRIBUTE = new ExpressionAttribute(null); - private final Expression expression; /** diff --git a/core/src/main/java/org/springframework/security/authorization/method/ExpressionUtils.java b/core/src/main/java/org/springframework/security/authorization/method/ExpressionUtils.java index fe6a64d959..97b540d3b6 100644 --- a/core/src/main/java/org/springframework/security/authorization/method/ExpressionUtils.java +++ b/core/src/main/java/org/springframework/security/authorization/method/ExpressionUtils.java @@ -16,6 +16,8 @@ package org.springframework.security.authorization.method; +import org.jspecify.annotations.Nullable; + import org.springframework.expression.EvaluationContext; import org.springframework.expression.EvaluationException; import org.springframework.expression.Expression; @@ -28,7 +30,7 @@ final class ExpressionUtils { private ExpressionUtils() { } - static AuthorizationResult evaluate(Expression expr, EvaluationContext ctx) { + static @Nullable AuthorizationResult evaluate(Expression expr, EvaluationContext ctx) { try { Object result = expr.getValue(ctx); if (result instanceof AuthorizationResult decision) { @@ -53,7 +55,7 @@ final class ExpressionUtils { } } - static AuthorizationDeniedException findAuthorizationException(EvaluationException ex) { + static @Nullable AuthorizationDeniedException findAuthorizationException(EvaluationException ex) { Throwable cause = ex.getCause(); while (cause != null) { if (cause instanceof AuthorizationDeniedException denied) { diff --git a/core/src/main/java/org/springframework/security/authorization/method/Jsr250AuthorizationManager.java b/core/src/main/java/org/springframework/security/authorization/method/Jsr250AuthorizationManager.java index 1012013e44..30310cce91 100644 --- a/core/src/main/java/org/springframework/security/authorization/method/Jsr250AuthorizationManager.java +++ b/core/src/main/java/org/springframework/security/authorization/method/Jsr250AuthorizationManager.java @@ -28,8 +28,8 @@ import jakarta.annotation.security.DenyAll; import jakarta.annotation.security.PermitAll; import jakarta.annotation.security.RolesAllowed; import org.aopalliance.intercept.MethodInvocation; +import org.jspecify.annotations.Nullable; -import org.springframework.lang.NonNull; import org.springframework.security.authorization.AuthoritiesAuthorizationManager; import org.springframework.security.authorization.AuthorizationManager; import org.springframework.security.authorization.AuthorizationResult; @@ -83,7 +83,8 @@ public final class Jsr250AuthorizationManager implements AuthorizationManager authentication, MethodInvocation methodInvocation) { + public @Nullable AuthorizationResult authorize(Supplier authentication, + MethodInvocation methodInvocation) { AuthorizationManager delegate = this.registry.getManager(methodInvocation); return delegate.authorize(authentication, methodInvocation); } @@ -93,9 +94,8 @@ public final class Jsr250AuthorizationManager implements AuthorizationManager scanner = SecurityAnnotationScanners .requireUnique(List.of(DenyAll.class, PermitAll.class, RolesAllowed.class)); - @NonNull @Override - AuthorizationManager resolveManager(Method method, Class targetClass) { + AuthorizationManager resolveManager(Method method, @Nullable Class targetClass) { Annotation annotation = findJsr250Annotation(method, targetClass); if (annotation instanceof DenyAll) { return SingleResultAuthorizationManager.denyAll(); @@ -104,14 +104,13 @@ public final class Jsr250AuthorizationManager implements AuthorizationManager) (a, - o) -> Jsr250AuthorizationManager.this.authoritiesAuthorizationManager.authorize(a, - getAllowedRolesWithPrefix(rolesAllowed)); + return (a, o) -> Jsr250AuthorizationManager.this.authoritiesAuthorizationManager.authorize(a, + getAllowedRolesWithPrefix(rolesAllowed)); } return NULL_MANAGER; } - private Annotation findJsr250Annotation(Method method, Class targetClass) { + private @Nullable Annotation findJsr250Annotation(Method method, @Nullable Class targetClass) { Class targetClassToUse = (targetClass != null) ? targetClass : method.getDeclaringClass(); return this.scanner.scan(method, targetClassToUse); } @@ -126,11 +125,4 @@ public final class Jsr250AuthorizationManager implements AuthorizationManager extends AuthorizationManager { - - @Override - AuthorizationResult authorize(Supplier authentication, T object); - - } - } diff --git a/core/src/main/java/org/springframework/security/authorization/method/MethodAuthorizationDeniedHandler.java b/core/src/main/java/org/springframework/security/authorization/method/MethodAuthorizationDeniedHandler.java index 5b059cf0b6..15a7e93c9b 100644 --- a/core/src/main/java/org/springframework/security/authorization/method/MethodAuthorizationDeniedHandler.java +++ b/core/src/main/java/org/springframework/security/authorization/method/MethodAuthorizationDeniedHandler.java @@ -17,8 +17,8 @@ package org.springframework.security.authorization.method; import org.aopalliance.intercept.MethodInvocation; +import org.jspecify.annotations.Nullable; -import org.springframework.lang.Nullable; import org.springframework.security.authorization.AuthorizationResult; /** @@ -41,8 +41,7 @@ public interface MethodAuthorizationDeniedHandler { * @return a replacement result for the denied method invocation, or null, or a * {@link reactor.core.publisher.Mono} for reactive applications */ - @Nullable - Object handleDeniedInvocation(MethodInvocation methodInvocation, AuthorizationResult authorizationResult); + @Nullable Object handleDeniedInvocation(MethodInvocation methodInvocation, AuthorizationResult authorizationResult); /** * Handle denied method invocations, implementations might either throw an @@ -56,8 +55,7 @@ public interface MethodAuthorizationDeniedHandler { * @return a replacement result for the denied method invocation, or null, or a * {@link reactor.core.publisher.Mono} for reactive applications */ - @Nullable - default Object handleDeniedInvocationResult(MethodInvocationResult methodInvocationResult, + default @Nullable Object handleDeniedInvocationResult(MethodInvocationResult methodInvocationResult, AuthorizationResult authorizationResult) { return handleDeniedInvocation(methodInvocationResult.getMethodInvocation(), authorizationResult); } diff --git a/core/src/main/java/org/springframework/security/authorization/method/MethodInvocationResult.java b/core/src/main/java/org/springframework/security/authorization/method/MethodInvocationResult.java index 35250f77df..76262d5f1f 100644 --- a/core/src/main/java/org/springframework/security/authorization/method/MethodInvocationResult.java +++ b/core/src/main/java/org/springframework/security/authorization/method/MethodInvocationResult.java @@ -17,6 +17,7 @@ package org.springframework.security.authorization.method; import org.aopalliance.intercept.MethodInvocation; +import org.jspecify.annotations.Nullable; import org.springframework.util.Assert; @@ -31,14 +32,14 @@ public class MethodInvocationResult { private final MethodInvocation methodInvocation; - private final Object result; + private @Nullable final Object result; /** * Construct a {@link MethodInvocationResult} with the provided parameters * @param methodInvocation the already-invoked {@link MethodInvocation} * @param result the value returned from the {@link MethodInvocation} */ - public MethodInvocationResult(MethodInvocation methodInvocation, Object result) { + public MethodInvocationResult(MethodInvocation methodInvocation, @Nullable Object result) { Assert.notNull(methodInvocation, "methodInvocation cannot be null"); this.methodInvocation = methodInvocation; this.result = result; @@ -56,7 +57,7 @@ public class MethodInvocationResult { * Return the result of the already-invoked {@link MethodInvocation} * @return the result */ - public Object getResult() { + public @Nullable Object getResult() { return this.result; } diff --git a/core/src/main/java/org/springframework/security/authorization/method/NullReturningMethodAuthorizationDeniedHandler.java b/core/src/main/java/org/springframework/security/authorization/method/NullReturningMethodAuthorizationDeniedHandler.java index f81a7cb50b..e11e01cc9b 100644 --- a/core/src/main/java/org/springframework/security/authorization/method/NullReturningMethodAuthorizationDeniedHandler.java +++ b/core/src/main/java/org/springframework/security/authorization/method/NullReturningMethodAuthorizationDeniedHandler.java @@ -17,6 +17,7 @@ package org.springframework.security.authorization.method; import org.aopalliance.intercept.MethodInvocation; +import org.jspecify.annotations.Nullable; import org.springframework.security.authorization.AuthorizationDeniedException; import org.springframework.security.authorization.AuthorizationResult; @@ -30,7 +31,8 @@ import org.springframework.security.authorization.AuthorizationResult; public final class NullReturningMethodAuthorizationDeniedHandler implements MethodAuthorizationDeniedHandler { @Override - public Object handleDeniedInvocation(MethodInvocation methodInvocation, AuthorizationResult authorizationResult) { + public @Nullable Object handleDeniedInvocation(MethodInvocation methodInvocation, + AuthorizationResult authorizationResult) { if (authorizationResult instanceof AuthorizationDeniedException exception) { throw exception; } @@ -38,7 +40,7 @@ public final class NullReturningMethodAuthorizationDeniedHandler implements Meth } @Override - public Object handleDeniedInvocationResult(MethodInvocationResult methodInvocationResult, + public @Nullable Object handleDeniedInvocationResult(MethodInvocationResult methodInvocationResult, AuthorizationResult authorizationResult) { if (authorizationResult instanceof AuthorizationDeniedException exception) { throw exception; diff --git a/core/src/main/java/org/springframework/security/authorization/method/PostAuthorizeAuthorizationManager.java b/core/src/main/java/org/springframework/security/authorization/method/PostAuthorizeAuthorizationManager.java index 84e9d1706c..c9e65aca6d 100644 --- a/core/src/main/java/org/springframework/security/authorization/method/PostAuthorizeAuthorizationManager.java +++ b/core/src/main/java/org/springframework/security/authorization/method/PostAuthorizeAuthorizationManager.java @@ -19,6 +19,7 @@ package org.springframework.security.authorization.method; import java.util.function.Supplier; import org.aopalliance.intercept.MethodInvocation; +import org.jspecify.annotations.Nullable; import org.springframework.context.ApplicationContext; import org.springframework.expression.EvaluationContext; @@ -85,9 +86,9 @@ public final class PostAuthorizeAuthorizationManager * {@link PostAuthorize} annotation is not present */ @Override - public AuthorizationResult authorize(Supplier authentication, MethodInvocationResult mi) { + public @Nullable AuthorizationResult authorize(Supplier authentication, MethodInvocationResult mi) { ExpressionAttribute attribute = this.registry.getAttribute(mi.getMethodInvocation()); - if (attribute == ExpressionAttribute.NULL_ATTRIBUTE) { + if (attribute == null) { return null; } MethodSecurityExpressionHandler expressionHandler = this.registry.getExpressionHandler(); @@ -97,14 +98,15 @@ public final class PostAuthorizeAuthorizationManager } @Override - public Object handleDeniedInvocation(MethodInvocation methodInvocation, AuthorizationResult authorizationResult) { + public @Nullable Object handleDeniedInvocation(MethodInvocation methodInvocation, + AuthorizationResult authorizationResult) { ExpressionAttribute attribute = this.registry.getAttribute(methodInvocation); PostAuthorizeExpressionAttribute postAuthorizeAttribute = (PostAuthorizeExpressionAttribute) attribute; return postAuthorizeAttribute.getHandler().handleDeniedInvocation(methodInvocation, authorizationResult); } @Override - public Object handleDeniedInvocationResult(MethodInvocationResult methodInvocationResult, + public @Nullable Object handleDeniedInvocationResult(MethodInvocationResult methodInvocationResult, AuthorizationResult authorizationResult) { ExpressionAttribute attribute = this.registry.getAttribute(methodInvocationResult.getMethodInvocation()); PostAuthorizeExpressionAttribute postAuthorizeAttribute = (PostAuthorizeExpressionAttribute) attribute; diff --git a/core/src/main/java/org/springframework/security/authorization/method/PostAuthorizeExpressionAttributeRegistry.java b/core/src/main/java/org/springframework/security/authorization/method/PostAuthorizeExpressionAttributeRegistry.java index 7dc96c106f..2b0fe02a87 100644 --- a/core/src/main/java/org/springframework/security/authorization/method/PostAuthorizeExpressionAttributeRegistry.java +++ b/core/src/main/java/org/springframework/security/authorization/method/PostAuthorizeExpressionAttributeRegistry.java @@ -20,7 +20,7 @@ import java.lang.reflect.Method; import java.util.Arrays; import java.util.function.Function; -import reactor.util.annotation.NonNull; +import org.jspecify.annotations.Nullable; import org.springframework.context.ApplicationContext; import org.springframework.expression.Expression; @@ -54,19 +54,18 @@ final class PostAuthorizeExpressionAttributeRegistry extends AbstractExpressionA PostAuthorizeAuthorizationManager.class); } - @NonNull @Override - ExpressionAttribute resolveAttribute(Method method, Class targetClass) { + @Nullable ExpressionAttribute resolveAttribute(Method method, @Nullable Class targetClass) { PostAuthorize postAuthorize = findPostAuthorizeAnnotation(method, targetClass); if (postAuthorize == null) { - return ExpressionAttribute.NULL_ATTRIBUTE; + return null; } Expression expression = getExpressionHandler().getExpressionParser().parseExpression(postAuthorize.value()); MethodAuthorizationDeniedHandler deniedHandler = resolveHandler(method, targetClass); return new PostAuthorizeExpressionAttribute(expression, deniedHandler); } - private MethodAuthorizationDeniedHandler resolveHandler(Method method, Class targetClass) { + private MethodAuthorizationDeniedHandler resolveHandler(Method method, @Nullable Class targetClass) { Class targetClassToUse = targetClass(method, targetClass); HandleAuthorizationDenied deniedHandler = this.handleAuthorizationDeniedScanner.scan(method, targetClassToUse); if (deniedHandler != null) { @@ -75,7 +74,7 @@ final class PostAuthorizeExpressionAttributeRegistry extends AbstractExpressionA return this.defaultHandler; } - private PostAuthorize findPostAuthorizeAnnotation(Method method, Class targetClass) { + private @Nullable PostAuthorize findPostAuthorizeAnnotation(Method method, @Nullable Class targetClass) { Class targetClassToUse = targetClass(method, targetClass); return this.postAuthorizeScanner.scan(method, targetClassToUse); } diff --git a/core/src/main/java/org/springframework/security/authorization/method/PostAuthorizeReactiveAuthorizationManager.java b/core/src/main/java/org/springframework/security/authorization/method/PostAuthorizeReactiveAuthorizationManager.java index 5dad5ba082..1624303ba9 100644 --- a/core/src/main/java/org/springframework/security/authorization/method/PostAuthorizeReactiveAuthorizationManager.java +++ b/core/src/main/java/org/springframework/security/authorization/method/PostAuthorizeReactiveAuthorizationManager.java @@ -17,6 +17,7 @@ package org.springframework.security.authorization.method; import org.aopalliance.intercept.MethodInvocation; +import org.jspecify.annotations.Nullable; import reactor.core.publisher.Mono; import org.springframework.context.ApplicationContext; @@ -81,7 +82,7 @@ public final class PostAuthorizeReactiveAuthorizationManager public Mono authorize(Mono authentication, MethodInvocationResult result) { MethodInvocation mi = result.getMethodInvocation(); ExpressionAttribute attribute = this.registry.getAttribute(mi); - if (attribute == ExpressionAttribute.NULL_ATTRIBUTE) { + if (attribute == null) { return Mono.empty(); } @@ -96,14 +97,15 @@ public final class PostAuthorizeReactiveAuthorizationManager } @Override - public Object handleDeniedInvocation(MethodInvocation methodInvocation, AuthorizationResult authorizationResult) { + public @Nullable Object handleDeniedInvocation(MethodInvocation methodInvocation, + AuthorizationResult authorizationResult) { ExpressionAttribute attribute = this.registry.getAttribute(methodInvocation); PostAuthorizeExpressionAttribute postAuthorizeAttribute = (PostAuthorizeExpressionAttribute) attribute; return postAuthorizeAttribute.getHandler().handleDeniedInvocation(methodInvocation, authorizationResult); } @Override - public Object handleDeniedInvocationResult(MethodInvocationResult methodInvocationResult, + public @Nullable Object handleDeniedInvocationResult(MethodInvocationResult methodInvocationResult, AuthorizationResult authorizationResult) { ExpressionAttribute attribute = this.registry.getAttribute(methodInvocationResult.getMethodInvocation()); PostAuthorizeExpressionAttribute postAuthorizeAttribute = (PostAuthorizeExpressionAttribute) attribute; diff --git a/core/src/main/java/org/springframework/security/authorization/method/PostFilterAuthorizationMethodInterceptor.java b/core/src/main/java/org/springframework/security/authorization/method/PostFilterAuthorizationMethodInterceptor.java index 275b0a0e27..eebca99a42 100644 --- a/core/src/main/java/org/springframework/security/authorization/method/PostFilterAuthorizationMethodInterceptor.java +++ b/core/src/main/java/org/springframework/security/authorization/method/PostFilterAuthorizationMethodInterceptor.java @@ -21,6 +21,7 @@ import java.util.function.Supplier; import org.aopalliance.aop.Advice; import org.aopalliance.intercept.MethodInterceptor; import org.aopalliance.intercept.MethodInvocation; +import org.jspecify.annotations.Nullable; import org.springframework.aop.Pointcut; import org.springframework.expression.EvaluationContext; @@ -126,10 +127,10 @@ public final class PostFilterAuthorizationMethodInterceptor implements Authoriza * @return filtered {@code returnedObject} */ @Override - public Object invoke(MethodInvocation mi) throws Throwable { + public @Nullable Object invoke(MethodInvocation mi) throws Throwable { Object returnedObject = mi.proceed(); ExpressionAttribute attribute = this.registry.getAttribute(mi); - if (attribute == ExpressionAttribute.NULL_ATTRIBUTE) { + if (attribute == null) { return returnedObject; } MethodSecurityExpressionHandler expressionHandler = this.registry.getExpressionHandler(); diff --git a/core/src/main/java/org/springframework/security/authorization/method/PostFilterAuthorizationReactiveMethodInterceptor.java b/core/src/main/java/org/springframework/security/authorization/method/PostFilterAuthorizationReactiveMethodInterceptor.java index 06c9e3d4f6..64cb729999 100644 --- a/core/src/main/java/org/springframework/security/authorization/method/PostFilterAuthorizationReactiveMethodInterceptor.java +++ b/core/src/main/java/org/springframework/security/authorization/method/PostFilterAuthorizationReactiveMethodInterceptor.java @@ -21,6 +21,7 @@ import java.lang.reflect.Method; import org.aopalliance.aop.Advice; import org.aopalliance.intercept.MethodInterceptor; import org.aopalliance.intercept.MethodInvocation; +import org.jspecify.annotations.Nullable; import org.reactivestreams.Publisher; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -29,6 +30,7 @@ import org.springframework.aop.Pointcut; import org.springframework.core.ReactiveAdapter; import org.springframework.core.ReactiveAdapterRegistry; import org.springframework.expression.EvaluationContext; +import org.springframework.expression.TypedValue; import org.springframework.security.access.expression.method.DefaultMethodSecurityExpressionHandler; import org.springframework.security.access.expression.method.MethodSecurityExpressionHandler; import org.springframework.security.access.expression.method.MethodSecurityExpressionOperations; @@ -86,9 +88,9 @@ public final class PostFilterAuthorizationReactiveMethodInterceptor implements A * @return the {@link Publisher} to use */ @Override - public Object invoke(MethodInvocation mi) throws Throwable { + public @Nullable Object invoke(MethodInvocation mi) throws Throwable { ExpressionAttribute attribute = this.registry.getAttribute(mi); - if (attribute == ExpressionAttribute.NULL_ATTRIBUTE) { + if (attribute == null) { return ReactiveMethodInvocationUtils.proceed(mi); } Mono toInvoke = ReactiveAuthenticationUtils.getAuthentication() @@ -112,7 +114,7 @@ public final class PostFilterAuthorizationReactiveMethodInterceptor implements A return (adapter != null) ? adapter.fromPublisher(mono) : mono; } - private boolean isMultiValue(Class returnType, ReactiveAdapter adapter) { + private boolean isMultiValue(Class returnType, @Nullable ReactiveAdapter adapter) { if (Flux.class.isAssignableFrom(returnType)) { return true; } @@ -132,7 +134,12 @@ public final class PostFilterAuthorizationReactiveMethodInterceptor implements A } private void setFilterObject(EvaluationContext ctx, Object result) { - ((MethodSecurityExpressionOperations) ctx.getRootObject().getValue()).setFilterObject(result); + TypedValue rootObject = ctx.getRootObject(); + Assert.notNull(rootObject, "rootObject cannot be null"); + MethodSecurityExpressionOperations methodOperations = (MethodSecurityExpressionOperations) rootObject + .getValue(); + Assert.notNull(methodOperations, "methodOperations cannot be null"); + methodOperations.setFilterObject(result); } private Mono postFilter(EvaluationContext ctx, Object result, ExpressionAttribute attribute) { diff --git a/core/src/main/java/org/springframework/security/authorization/method/PostFilterExpressionAttributeRegistry.java b/core/src/main/java/org/springframework/security/authorization/method/PostFilterExpressionAttributeRegistry.java index 3d6881a8d3..7c0fd85da8 100644 --- a/core/src/main/java/org/springframework/security/authorization/method/PostFilterExpressionAttributeRegistry.java +++ b/core/src/main/java/org/springframework/security/authorization/method/PostFilterExpressionAttributeRegistry.java @@ -18,8 +18,9 @@ package org.springframework.security.authorization.method; import java.lang.reflect.Method; +import org.jspecify.annotations.Nullable; + import org.springframework.expression.Expression; -import org.springframework.lang.NonNull; import org.springframework.security.access.prepost.PostFilter; import org.springframework.security.core.annotation.AnnotationTemplateExpressionDefaults; import org.springframework.security.core.annotation.SecurityAnnotationScanner; @@ -36,12 +37,11 @@ final class PostFilterExpressionAttributeRegistry extends AbstractExpressionAttr private SecurityAnnotationScanner scanner = SecurityAnnotationScanners.requireUnique(PostFilter.class); - @NonNull @Override - ExpressionAttribute resolveAttribute(Method method, Class targetClass) { + @Nullable ExpressionAttribute resolveAttribute(Method method, @Nullable Class targetClass) { PostFilter postFilter = findPostFilterAnnotation(method, targetClass); if (postFilter == null) { - return ExpressionAttribute.NULL_ATTRIBUTE; + return null; } Expression postFilterExpression = getExpressionHandler().getExpressionParser() .parseExpression(postFilter.value()); @@ -52,7 +52,7 @@ final class PostFilterExpressionAttributeRegistry extends AbstractExpressionAttr this.scanner = SecurityAnnotationScanners.requireUnique(PostFilter.class, defaults); } - private PostFilter findPostFilterAnnotation(Method method, Class targetClass) { + private @Nullable PostFilter findPostFilterAnnotation(Method method, @Nullable Class targetClass) { Class targetClassToUse = targetClass(method, targetClass); return this.scanner.scan(method, targetClassToUse); } diff --git a/core/src/main/java/org/springframework/security/authorization/method/PreAuthorizeAuthorizationManager.java b/core/src/main/java/org/springframework/security/authorization/method/PreAuthorizeAuthorizationManager.java index b23a97c5fe..1e04733aeb 100644 --- a/core/src/main/java/org/springframework/security/authorization/method/PreAuthorizeAuthorizationManager.java +++ b/core/src/main/java/org/springframework/security/authorization/method/PreAuthorizeAuthorizationManager.java @@ -19,6 +19,7 @@ package org.springframework.security.authorization.method; import java.util.function.Supplier; import org.aopalliance.intercept.MethodInvocation; +import org.jspecify.annotations.Nullable; import org.springframework.context.ApplicationContext; import org.springframework.expression.EvaluationContext; @@ -77,9 +78,9 @@ public final class PreAuthorizeAuthorizationManager * {@link PreAuthorize} annotation is not present */ @Override - public AuthorizationResult authorize(Supplier authentication, MethodInvocation mi) { + public @Nullable AuthorizationResult authorize(Supplier authentication, MethodInvocation mi) { ExpressionAttribute attribute = this.registry.getAttribute(mi); - if (attribute == ExpressionAttribute.NULL_ATTRIBUTE) { + if (attribute == null) { return null; } EvaluationContext ctx = this.registry.getExpressionHandler().createEvaluationContext(authentication, mi); @@ -87,7 +88,8 @@ public final class PreAuthorizeAuthorizationManager } @Override - public Object handleDeniedInvocation(MethodInvocation methodInvocation, AuthorizationResult authorizationResult) { + public @Nullable Object handleDeniedInvocation(MethodInvocation methodInvocation, + AuthorizationResult authorizationResult) { ExpressionAttribute attribute = this.registry.getAttribute(methodInvocation); PreAuthorizeExpressionAttribute preAuthorizeAttribute = (PreAuthorizeExpressionAttribute) attribute; return preAuthorizeAttribute.getHandler().handleDeniedInvocation(methodInvocation, authorizationResult); diff --git a/core/src/main/java/org/springframework/security/authorization/method/PreAuthorizeExpressionAttributeRegistry.java b/core/src/main/java/org/springframework/security/authorization/method/PreAuthorizeExpressionAttributeRegistry.java index b2700cef34..3abeb79fee 100644 --- a/core/src/main/java/org/springframework/security/authorization/method/PreAuthorizeExpressionAttributeRegistry.java +++ b/core/src/main/java/org/springframework/security/authorization/method/PreAuthorizeExpressionAttributeRegistry.java @@ -20,9 +20,10 @@ import java.lang.reflect.Method; import java.util.Arrays; import java.util.function.Function; +import org.jspecify.annotations.Nullable; + import org.springframework.context.ApplicationContext; import org.springframework.expression.Expression; -import org.springframework.lang.NonNull; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.core.annotation.AnnotationTemplateExpressionDefaults; import org.springframework.security.core.annotation.SecurityAnnotationScanner; @@ -53,19 +54,18 @@ final class PreAuthorizeExpressionAttributeRegistry extends AbstractExpressionAt PreAuthorizeAuthorizationManager.class); } - @NonNull @Override - ExpressionAttribute resolveAttribute(Method method, Class targetClass) { + @Nullable ExpressionAttribute resolveAttribute(Method method, @Nullable Class targetClass) { PreAuthorize preAuthorize = findPreAuthorizeAnnotation(method, targetClass); if (preAuthorize == null) { - return ExpressionAttribute.NULL_ATTRIBUTE; + return null; } Expression expression = getExpressionHandler().getExpressionParser().parseExpression(preAuthorize.value()); MethodAuthorizationDeniedHandler handler = resolveHandler(method, targetClass); return new PreAuthorizeExpressionAttribute(expression, handler); } - private MethodAuthorizationDeniedHandler resolveHandler(Method method, Class targetClass) { + private MethodAuthorizationDeniedHandler resolveHandler(Method method, @Nullable Class targetClass) { Class targetClassToUse = targetClass(method, targetClass); HandleAuthorizationDenied deniedHandler = this.handleAuthorizationDeniedScanner.scan(method, targetClassToUse); if (deniedHandler != null) { @@ -74,7 +74,7 @@ final class PreAuthorizeExpressionAttributeRegistry extends AbstractExpressionAt return this.defaultHandler; } - private PreAuthorize findPreAuthorizeAnnotation(Method method, Class targetClass) { + private @Nullable PreAuthorize findPreAuthorizeAnnotation(Method method, @Nullable Class targetClass) { Class targetClassToUse = targetClass(method, targetClass); return this.preAuthorizeScanner.scan(method, targetClassToUse); } diff --git a/core/src/main/java/org/springframework/security/authorization/method/PreAuthorizeReactiveAuthorizationManager.java b/core/src/main/java/org/springframework/security/authorization/method/PreAuthorizeReactiveAuthorizationManager.java index b5c573283f..c87c183002 100644 --- a/core/src/main/java/org/springframework/security/authorization/method/PreAuthorizeReactiveAuthorizationManager.java +++ b/core/src/main/java/org/springframework/security/authorization/method/PreAuthorizeReactiveAuthorizationManager.java @@ -17,6 +17,7 @@ package org.springframework.security.authorization.method; import org.aopalliance.intercept.MethodInvocation; +import org.jspecify.annotations.Nullable; import reactor.core.publisher.Mono; import org.springframework.context.ApplicationContext; @@ -78,7 +79,7 @@ public final class PreAuthorizeReactiveAuthorizationManager @Override public Mono authorize(Mono authentication, MethodInvocation mi) { ExpressionAttribute attribute = this.registry.getAttribute(mi); - if (attribute == ExpressionAttribute.NULL_ATTRIBUTE) { + if (attribute == null) { return Mono.empty(); } // @formatter:off @@ -90,7 +91,8 @@ public final class PreAuthorizeReactiveAuthorizationManager } @Override - public Object handleDeniedInvocation(MethodInvocation methodInvocation, AuthorizationResult authorizationResult) { + public @Nullable Object handleDeniedInvocation(MethodInvocation methodInvocation, + AuthorizationResult authorizationResult) { ExpressionAttribute attribute = this.registry.getAttribute(methodInvocation); PreAuthorizeExpressionAttribute preAuthorizeAttribute = (PreAuthorizeExpressionAttribute) attribute; return preAuthorizeAttribute.getHandler().handleDeniedInvocation(methodInvocation, authorizationResult); diff --git a/core/src/main/java/org/springframework/security/authorization/method/PreFilterAuthorizationMethodInterceptor.java b/core/src/main/java/org/springframework/security/authorization/method/PreFilterAuthorizationMethodInterceptor.java index 12f9875bbd..a1b316e480 100644 --- a/core/src/main/java/org/springframework/security/authorization/method/PreFilterAuthorizationMethodInterceptor.java +++ b/core/src/main/java/org/springframework/security/authorization/method/PreFilterAuthorizationMethodInterceptor.java @@ -21,6 +21,7 @@ import java.util.function.Supplier; import org.aopalliance.aop.Advice; import org.aopalliance.intercept.MethodInterceptor; import org.aopalliance.intercept.MethodInvocation; +import org.jspecify.annotations.Nullable; import org.springframework.aop.Pointcut; import org.springframework.expression.EvaluationContext; @@ -126,9 +127,9 @@ public final class PreFilterAuthorizationMethodInterceptor implements Authorizat * @param mi the {@link MethodInvocation} to check */ @Override - public Object invoke(MethodInvocation mi) throws Throwable { + public @Nullable Object invoke(MethodInvocation mi) throws Throwable { PreFilterExpressionAttributeRegistry.PreFilterExpressionAttribute attribute = this.registry.getAttribute(mi); - if (attribute == PreFilterExpressionAttributeRegistry.PreFilterExpressionAttribute.NULL_ATTRIBUTE) { + if (attribute == null) { return mi.proceed(); } MethodSecurityExpressionHandler expressionHandler = this.registry.getExpressionHandler(); diff --git a/core/src/main/java/org/springframework/security/authorization/method/PreFilterAuthorizationReactiveMethodInterceptor.java b/core/src/main/java/org/springframework/security/authorization/method/PreFilterAuthorizationReactiveMethodInterceptor.java index 9532d74f33..3d8d41fd53 100644 --- a/core/src/main/java/org/springframework/security/authorization/method/PreFilterAuthorizationReactiveMethodInterceptor.java +++ b/core/src/main/java/org/springframework/security/authorization/method/PreFilterAuthorizationReactiveMethodInterceptor.java @@ -21,6 +21,7 @@ import java.lang.reflect.Method; import org.aopalliance.aop.Advice; import org.aopalliance.intercept.MethodInterceptor; import org.aopalliance.intercept.MethodInvocation; +import org.jspecify.annotations.Nullable; import org.reactivestreams.Publisher; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -98,9 +99,9 @@ public final class PreFilterAuthorizationReactiveMethodInterceptor implements Au * @return the {@link Publisher} to use */ @Override - public Object invoke(MethodInvocation mi) throws Throwable { + public @Nullable Object invoke(MethodInvocation mi) throws Throwable { PreFilterExpressionAttributeRegistry.PreFilterExpressionAttribute attribute = this.registry.getAttribute(mi); - if (attribute == PreFilterExpressionAttributeRegistry.PreFilterExpressionAttribute.NULL_ATTRIBUTE) { + if (attribute == null) { return ReactiveMethodInvocationUtils.proceed(mi); } FilterTarget filterTarget = findFilterTarget(attribute.getFilterTarget(), mi); @@ -160,7 +161,7 @@ public final class PreFilterAuthorizationReactiveMethodInterceptor implements Au return new FilterTarget((Publisher) value, index); } - private boolean isMultiValue(Class returnType, ReactiveAdapter adapter) { + private boolean isMultiValue(Class returnType, @Nullable ReactiveAdapter adapter) { if (Flux.class.isAssignableFrom(returnType)) { return true; } @@ -171,7 +172,9 @@ public final class PreFilterAuthorizationReactiveMethodInterceptor implements Au MethodSecurityExpressionOperations rootObject = (MethodSecurityExpressionOperations) ctx.getRootObject() .getValue(); return Mono.from(filterTarget).filterWhen((filterObject) -> { - rootObject.setFilterObject(filterObject); + if (rootObject != null) { + rootObject.setFilterObject(filterObject); + } return ReactiveExpressionUtils.evaluateAsBoolean(filterExpression, ctx); }); } @@ -180,7 +183,9 @@ public final class PreFilterAuthorizationReactiveMethodInterceptor implements Au MethodSecurityExpressionOperations rootObject = (MethodSecurityExpressionOperations) ctx.getRootObject() .getValue(); return Flux.from(filterTarget).filterWhen((filterObject) -> { - rootObject.setFilterObject(filterObject); + if (rootObject != null) { + rootObject.setFilterObject(filterObject); + } return ReactiveExpressionUtils.evaluateAsBoolean(filterExpression, ctx); }); } diff --git a/core/src/main/java/org/springframework/security/authorization/method/PreFilterExpressionAttributeRegistry.java b/core/src/main/java/org/springframework/security/authorization/method/PreFilterExpressionAttributeRegistry.java index 654c6c2514..9f7cff0405 100644 --- a/core/src/main/java/org/springframework/security/authorization/method/PreFilterExpressionAttributeRegistry.java +++ b/core/src/main/java/org/springframework/security/authorization/method/PreFilterExpressionAttributeRegistry.java @@ -18,8 +18,9 @@ package org.springframework.security.authorization.method; import java.lang.reflect.Method; +import org.jspecify.annotations.Nullable; + import org.springframework.expression.Expression; -import org.springframework.lang.NonNull; import org.springframework.security.access.prepost.PreFilter; import org.springframework.security.core.annotation.AnnotationTemplateExpressionDefaults; import org.springframework.security.core.annotation.SecurityAnnotationScanner; @@ -37,12 +38,11 @@ final class PreFilterExpressionAttributeRegistry private SecurityAnnotationScanner scanner = SecurityAnnotationScanners.requireUnique(PreFilter.class); - @NonNull @Override - PreFilterExpressionAttribute resolveAttribute(Method method, Class targetClass) { + @Nullable PreFilterExpressionAttribute resolveAttribute(Method method, @Nullable Class targetClass) { PreFilter preFilter = findPreFilterAnnotation(method, targetClass); if (preFilter == null) { - return PreFilterExpressionAttribute.NULL_ATTRIBUTE; + return null; } Expression preFilterExpression = getExpressionHandler().getExpressionParser() .parseExpression(preFilter.value()); @@ -53,15 +53,13 @@ final class PreFilterExpressionAttributeRegistry this.scanner = SecurityAnnotationScanners.requireUnique(PreFilter.class, defaults); } - private PreFilter findPreFilterAnnotation(Method method, Class targetClass) { + private @Nullable PreFilter findPreFilterAnnotation(Method method, @Nullable Class targetClass) { Class targetClassToUse = targetClass(method, targetClass); return this.scanner.scan(method, targetClassToUse); } static final class PreFilterExpressionAttribute extends ExpressionAttribute { - static final PreFilterExpressionAttribute NULL_ATTRIBUTE = new PreFilterExpressionAttribute(null, null); - private final String filterTarget; private PreFilterExpressionAttribute(Expression expression, String filterTarget) { diff --git a/core/src/main/java/org/springframework/security/authorization/method/ReactiveExpressionUtils.java b/core/src/main/java/org/springframework/security/authorization/method/ReactiveExpressionUtils.java index 6c80c0d364..365caf5976 100644 --- a/core/src/main/java/org/springframework/security/authorization/method/ReactiveExpressionUtils.java +++ b/core/src/main/java/org/springframework/security/authorization/method/ReactiveExpressionUtils.java @@ -16,6 +16,7 @@ package org.springframework.security.authorization.method; +import org.jspecify.annotations.Nullable; import reactor.core.publisher.Mono; import org.springframework.expression.EvaluationContext; @@ -49,7 +50,7 @@ final class ReactiveExpressionUtils { }); } - private static Mono adapt(Expression expr, Object value) { + private static Mono adapt(Expression expr, @Nullable Object value) { if (value instanceof Boolean granted) { return Mono.just(new ExpressionAuthorizationDecision(granted, expr)); } diff --git a/core/src/main/java/org/springframework/security/authorization/method/ReactiveMethodInvocationUtils.java b/core/src/main/java/org/springframework/security/authorization/method/ReactiveMethodInvocationUtils.java index 7b484525ab..5ead0f8818 100644 --- a/core/src/main/java/org/springframework/security/authorization/method/ReactiveMethodInvocationUtils.java +++ b/core/src/main/java/org/springframework/security/authorization/method/ReactiveMethodInvocationUtils.java @@ -17,6 +17,7 @@ package org.springframework.security.authorization.method; import org.aopalliance.intercept.MethodInvocation; +import org.jspecify.annotations.Nullable; import reactor.core.Exceptions; /** @@ -27,7 +28,7 @@ import reactor.core.Exceptions; */ final class ReactiveMethodInvocationUtils { - static T proceed(MethodInvocation mi) { + static @Nullable T proceed(MethodInvocation mi) { try { return (T) mi.proceed(); } diff --git a/core/src/main/java/org/springframework/security/authorization/method/ReflectiveMethodAuthorizationDeniedHandler.java b/core/src/main/java/org/springframework/security/authorization/method/ReflectiveMethodAuthorizationDeniedHandler.java index 25fd350e64..e9893595dc 100644 --- a/core/src/main/java/org/springframework/security/authorization/method/ReflectiveMethodAuthorizationDeniedHandler.java +++ b/core/src/main/java/org/springframework/security/authorization/method/ReflectiveMethodAuthorizationDeniedHandler.java @@ -19,6 +19,7 @@ package org.springframework.security.authorization.method; import org.aopalliance.intercept.MethodInvocation; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.Nullable; import org.springframework.security.authorization.AuthorizationResult; @@ -39,13 +40,14 @@ final class ReflectiveMethodAuthorizationDeniedHandler implements MethodAuthoriz } @Override - public Object handleDeniedInvocation(MethodInvocation methodInvocation, AuthorizationResult authorizationResult) { + public @Nullable Object handleDeniedInvocation(MethodInvocation methodInvocation, + AuthorizationResult authorizationResult) { return constructMethodAuthorizationDeniedHandler().handleDeniedInvocation(methodInvocation, authorizationResult); } @Override - public Object handleDeniedInvocationResult(MethodInvocationResult methodInvocationResult, + public @Nullable Object handleDeniedInvocationResult(MethodInvocationResult methodInvocationResult, AuthorizationResult authorizationResult) { return constructMethodAuthorizationDeniedHandler().handleDeniedInvocationResult(methodInvocationResult, authorizationResult); diff --git a/core/src/main/java/org/springframework/security/authorization/method/SecuredAuthorizationManager.java b/core/src/main/java/org/springframework/security/authorization/method/SecuredAuthorizationManager.java index d36c268a33..8c5f6933cb 100644 --- a/core/src/main/java/org/springframework/security/authorization/method/SecuredAuthorizationManager.java +++ b/core/src/main/java/org/springframework/security/authorization/method/SecuredAuthorizationManager.java @@ -25,6 +25,7 @@ import java.util.concurrent.ConcurrentHashMap; import java.util.function.Supplier; import org.aopalliance.intercept.MethodInvocation; +import org.jspecify.annotations.Nullable; import org.springframework.core.MethodClassKey; import org.springframework.security.access.annotation.Secured; @@ -67,7 +68,7 @@ public final class SecuredAuthorizationManager implements AuthorizationManager authentication, MethodInvocation mi) { + public @Nullable AuthorizationResult authorize(Supplier authentication, MethodInvocation mi) { Set authorities = getAuthorities(mi); return authorities.isEmpty() ? null : this.authoritiesAuthorizationManager.authorize(authentication, authorities); @@ -81,12 +82,12 @@ public final class SecuredAuthorizationManager implements AuthorizationManager resolveAuthorities(method, targetClass)); } - private Set resolveAuthorities(Method method, Class targetClass) { + private Set resolveAuthorities(Method method, @Nullable Class targetClass) { Secured secured = findSecuredAnnotation(method, targetClass); return (secured != null) ? Set.of(secured.value()) : Collections.emptySet(); } - private Secured findSecuredAnnotation(Method method, Class targetClass) { + private @Nullable Secured findSecuredAnnotation(Method method, @Nullable Class targetClass) { Class targetClassToUse = (targetClass != null) ? targetClass : method.getDeclaringClass(); return this.scanner.scan(method, targetClassToUse); } diff --git a/core/src/main/java/org/springframework/security/authorization/method/package-info.java b/core/src/main/java/org/springframework/security/authorization/method/package-info.java new file mode 100644 index 0000000000..cc8dbb21eb --- /dev/null +++ b/core/src/main/java/org/springframework/security/authorization/method/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright 2002-2025 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.authorization.method; + +import org.jspecify.annotations.NullMarked; diff --git a/core/src/main/java/org/springframework/security/authorization/package-info.java b/core/src/main/java/org/springframework/security/authorization/package-info.java new file mode 100644 index 0000000000..f188883232 --- /dev/null +++ b/core/src/main/java/org/springframework/security/authorization/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright 2002-2025 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.authorization; + +import org.jspecify.annotations.NullMarked; diff --git a/core/src/main/java/org/springframework/security/concurrent/AbstractDelegatingSecurityContextSupport.java b/core/src/main/java/org/springframework/security/concurrent/AbstractDelegatingSecurityContextSupport.java index b02bef249f..e00226ef93 100644 --- a/core/src/main/java/org/springframework/security/concurrent/AbstractDelegatingSecurityContextSupport.java +++ b/core/src/main/java/org/springframework/security/concurrent/AbstractDelegatingSecurityContextSupport.java @@ -18,6 +18,8 @@ package org.springframework.security.concurrent; import java.util.concurrent.Callable; +import org.jspecify.annotations.Nullable; + import org.springframework.security.core.context.SecurityContext; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.context.SecurityContextHolderStrategy; @@ -36,7 +38,7 @@ abstract class AbstractDelegatingSecurityContextSupport { private SecurityContextHolderStrategy securityContextHolderStrategy = SecurityContextHolder .getContextHolderStrategy(); - private final SecurityContext securityContext; + private final @Nullable SecurityContext securityContext; /** * Creates a new {@link AbstractDelegatingSecurityContextSupport} that uses the @@ -46,7 +48,7 @@ abstract class AbstractDelegatingSecurityContextSupport { * {@link DelegatingSecurityContextCallable} or null to default to the current * {@link SecurityContext}. */ - AbstractDelegatingSecurityContextSupport(SecurityContext securityContext) { + AbstractDelegatingSecurityContextSupport(@Nullable SecurityContext securityContext) { this.securityContext = securityContext; } diff --git a/core/src/main/java/org/springframework/security/concurrent/DelegatingSecurityContextCallable.java b/core/src/main/java/org/springframework/security/concurrent/DelegatingSecurityContextCallable.java index ee249f3e6b..52484528fa 100644 --- a/core/src/main/java/org/springframework/security/concurrent/DelegatingSecurityContextCallable.java +++ b/core/src/main/java/org/springframework/security/concurrent/DelegatingSecurityContextCallable.java @@ -18,6 +18,8 @@ package org.springframework.security.concurrent; import java.util.concurrent.Callable; +import org.jspecify.annotations.Nullable; + import org.springframework.security.core.context.SecurityContext; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.context.SecurityContextHolderStrategy; @@ -55,7 +57,7 @@ public final class DelegatingSecurityContextCallable implements Callable { * The {@link SecurityContext} that was on the {@link SecurityContextHolder} prior to * being set to the delegateSecurityContext. */ - private SecurityContext originalSecurityContext; + private @Nullable SecurityContext originalSecurityContext; /** * Creates a new {@link DelegatingSecurityContextCallable} with a specific @@ -142,7 +144,7 @@ public final class DelegatingSecurityContextCallable implements Callable { : new DelegatingSecurityContextCallable<>(delegate); } - static Callable create(Callable delegate, SecurityContext securityContext, + static Callable create(Callable delegate, @Nullable SecurityContext securityContext, SecurityContextHolderStrategy securityContextHolderStrategy) { Assert.notNull(delegate, "delegate cannot be null"); Assert.notNull(securityContextHolderStrategy, "securityContextHolderStrategy cannot be null"); diff --git a/core/src/main/java/org/springframework/security/concurrent/DelegatingSecurityContextExecutor.java b/core/src/main/java/org/springframework/security/concurrent/DelegatingSecurityContextExecutor.java index f4d0545d23..b52ba6ed9a 100644 --- a/core/src/main/java/org/springframework/security/concurrent/DelegatingSecurityContextExecutor.java +++ b/core/src/main/java/org/springframework/security/concurrent/DelegatingSecurityContextExecutor.java @@ -18,6 +18,8 @@ package org.springframework.security.concurrent; import java.util.concurrent.Executor; +import org.jspecify.annotations.Nullable; + import org.springframework.security.core.context.SecurityContext; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.context.SecurityContextHolderStrategy; @@ -42,7 +44,7 @@ public class DelegatingSecurityContextExecutor extends AbstractDelegatingSecurit * {@link DelegatingSecurityContextRunnable} or null to default to the current * {@link SecurityContext} */ - public DelegatingSecurityContextExecutor(Executor delegateExecutor, SecurityContext securityContext) { + public DelegatingSecurityContextExecutor(Executor delegateExecutor, @Nullable SecurityContext securityContext) { super(securityContext); Assert.notNull(delegateExecutor, "delegateExecutor cannot be null"); this.delegate = delegateExecutor; diff --git a/core/src/main/java/org/springframework/security/concurrent/DelegatingSecurityContextExecutorService.java b/core/src/main/java/org/springframework/security/concurrent/DelegatingSecurityContextExecutorService.java index 289f9bec83..40b4f264ec 100644 --- a/core/src/main/java/org/springframework/security/concurrent/DelegatingSecurityContextExecutorService.java +++ b/core/src/main/java/org/springframework/security/concurrent/DelegatingSecurityContextExecutorService.java @@ -26,6 +26,8 @@ import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; +import org.jspecify.annotations.Nullable; + import org.springframework.security.core.context.SecurityContext; import org.springframework.security.core.context.SecurityContextHolder; @@ -50,7 +52,7 @@ public class DelegatingSecurityContextExecutorService extends DelegatingSecurity * {@link DelegatingSecurityContextCallable}. */ public DelegatingSecurityContextExecutorService(ExecutorService delegateExecutorService, - SecurityContext securityContext) { + @Nullable SecurityContext securityContext) { super(delegateExecutorService, securityContext); } @@ -132,7 +134,7 @@ public class DelegatingSecurityContextExecutorService extends DelegatingSecurity return getDelegate().invokeAny(tasks, timeout, unit); } - private Collection> createTasks(Collection> tasks) { + private @Nullable Collection> createTasks(Collection> tasks) { if (tasks == null) { return null; } diff --git a/core/src/main/java/org/springframework/security/concurrent/DelegatingSecurityContextRunnable.java b/core/src/main/java/org/springframework/security/concurrent/DelegatingSecurityContextRunnable.java index cc32994939..cd7744c789 100644 --- a/core/src/main/java/org/springframework/security/concurrent/DelegatingSecurityContextRunnable.java +++ b/core/src/main/java/org/springframework/security/concurrent/DelegatingSecurityContextRunnable.java @@ -16,6 +16,8 @@ package org.springframework.security.concurrent; +import org.jspecify.annotations.Nullable; + import org.springframework.security.core.context.SecurityContext; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.context.SecurityContextHolderStrategy; @@ -53,7 +55,7 @@ public final class DelegatingSecurityContextRunnable implements Runnable { * The {@link SecurityContext} that was on the {@link SecurityContextHolder} prior to * being set to the delegateSecurityContext. */ - private SecurityContext originalSecurityContext; + private @Nullable SecurityContext originalSecurityContext; /** * Creates a new {@link DelegatingSecurityContextRunnable} with a specific @@ -134,13 +136,13 @@ public final class DelegatingSecurityContextRunnable implements Runnable { * {@link SecurityContextHolder} will be used. * @return */ - public static Runnable create(Runnable delegate, SecurityContext securityContext) { + public static Runnable create(Runnable delegate, @Nullable SecurityContext securityContext) { Assert.notNull(delegate, "delegate cannot be null"); return (securityContext != null) ? new DelegatingSecurityContextRunnable(delegate, securityContext) : new DelegatingSecurityContextRunnable(delegate); } - static Runnable create(Runnable delegate, SecurityContext securityContext, + static Runnable create(Runnable delegate, @Nullable SecurityContext securityContext, SecurityContextHolderStrategy securityContextHolderStrategy) { Assert.notNull(delegate, "delegate cannot be null"); Assert.notNull(securityContextHolderStrategy, "securityContextHolderStrategy cannot be null"); diff --git a/core/src/main/java/org/springframework/security/concurrent/DelegatingSecurityContextScheduledExecutorService.java b/core/src/main/java/org/springframework/security/concurrent/DelegatingSecurityContextScheduledExecutorService.java index ee8ff98489..c3c3621e93 100644 --- a/core/src/main/java/org/springframework/security/concurrent/DelegatingSecurityContextScheduledExecutorService.java +++ b/core/src/main/java/org/springframework/security/concurrent/DelegatingSecurityContextScheduledExecutorService.java @@ -21,6 +21,8 @@ import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; +import org.jspecify.annotations.Nullable; + import org.springframework.security.core.context.SecurityContext; import org.springframework.security.core.context.SecurityContextHolder; @@ -45,7 +47,7 @@ public final class DelegatingSecurityContextScheduledExecutorService extends Del * {@link DelegatingSecurityContextCallable}. */ public DelegatingSecurityContextScheduledExecutorService(ScheduledExecutorService delegateScheduledExecutorService, - SecurityContext securityContext) { + @Nullable SecurityContext securityContext) { super(delegateScheduledExecutorService, securityContext); } diff --git a/core/src/main/java/org/springframework/security/concurrent/package-info.java b/core/src/main/java/org/springframework/security/concurrent/package-info.java new file mode 100644 index 0000000000..690bee2367 --- /dev/null +++ b/core/src/main/java/org/springframework/security/concurrent/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright 2002-2025 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.concurrent; + +import org.jspecify.annotations.NullMarked; diff --git a/core/src/main/java/org/springframework/security/context/package-info.java b/core/src/main/java/org/springframework/security/context/package-info.java new file mode 100644 index 0000000000..e0c007ae25 --- /dev/null +++ b/core/src/main/java/org/springframework/security/context/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright 2002-2025 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.context; + +import org.jspecify.annotations.NullMarked; diff --git a/core/src/main/java/org/springframework/security/converter/RsaKeyConverters.java b/core/src/main/java/org/springframework/security/converter/RsaKeyConverters.java index 02ba68e67d..775150e233 100644 --- a/core/src/main/java/org/springframework/security/converter/RsaKeyConverters.java +++ b/core/src/main/java/org/springframework/security/converter/RsaKeyConverters.java @@ -34,8 +34,9 @@ import java.util.Base64; import java.util.List; import java.util.stream.Collectors; +import org.jspecify.annotations.NonNull; + import org.springframework.core.convert.converter.Converter; -import org.springframework.lang.NonNull; import org.springframework.util.Assert; /** @@ -162,8 +163,7 @@ public final class RsaKeyConverters { } @Override - @NonNull - public RSAPublicKey convert(List lines) { + public @NonNull RSAPublicKey convert(List lines) { StringBuilder base64Encoded = new StringBuilder(); for (String line : lines) { if (isNotX509PemWrapper(line)) { @@ -194,8 +194,7 @@ public final class RsaKeyConverters { } @Override - @NonNull - public RSAPublicKey convert(List lines) { + public @NonNull RSAPublicKey convert(List lines) { StringBuilder base64Encoded = new StringBuilder(); for (String line : lines) { if (isNotX509CertificateWrapper(line)) { diff --git a/core/src/main/java/org/springframework/security/converter/package-info.java b/core/src/main/java/org/springframework/security/converter/package-info.java new file mode 100644 index 0000000000..f30f4ca71e --- /dev/null +++ b/core/src/main/java/org/springframework/security/converter/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright 2002-2025 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.converter; + +import org.jspecify.annotations.NullMarked; diff --git a/core/src/main/java/org/springframework/security/core/Authentication.java b/core/src/main/java/org/springframework/security/core/Authentication.java index d3f38a5c11..c8515b25fc 100644 --- a/core/src/main/java/org/springframework/security/core/Authentication.java +++ b/core/src/main/java/org/springframework/security/core/Authentication.java @@ -20,6 +20,8 @@ import java.io.Serializable; import java.security.Principal; import java.util.Collection; +import org.jspecify.annotations.Nullable; + import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.core.context.SecurityContextHolder; @@ -72,7 +74,7 @@ public interface Authentication extends Principal, Serializable { * are expected to populate the credentials. * @return the credentials that prove the identity of the Principal */ - Object getCredentials(); + @Nullable Object getCredentials(); /** * Stores additional details about the authentication request. These might be an IP @@ -80,7 +82,7 @@ public interface Authentication extends Principal, Serializable { * @return additional details about the authentication request, or null * if not used */ - Object getDetails(); + @Nullable Object getDetails(); /** * The identity of the principal being authenticated. In the case of an authentication @@ -94,7 +96,7 @@ public interface Authentication extends Principal, Serializable { * @return the Principal being authenticated or the authenticated * principal after authentication. */ - Object getPrincipal(); + @Nullable Object getPrincipal(); /** * Used to indicate to {@code AbstractSecurityInterceptor} whether it should present diff --git a/core/src/main/java/org/springframework/security/core/AuthenticationException.java b/core/src/main/java/org/springframework/security/core/AuthenticationException.java index 8efe1be55f..0a4fbaff60 100644 --- a/core/src/main/java/org/springframework/security/core/AuthenticationException.java +++ b/core/src/main/java/org/springframework/security/core/AuthenticationException.java @@ -18,6 +18,8 @@ package org.springframework.security.core; import java.io.Serial; +import org.jspecify.annotations.Nullable; + import org.springframework.util.Assert; /** @@ -31,7 +33,7 @@ public abstract class AuthenticationException extends RuntimeException { @Serial private static final long serialVersionUID = 2018827803361503060L; - private Authentication authenticationRequest; + private @Nullable Authentication authenticationRequest; /** * Constructs an {@code AuthenticationException} with the specified message and root @@ -39,7 +41,7 @@ public abstract class AuthenticationException extends RuntimeException { * @param msg the detail message * @param cause the root cause */ - public AuthenticationException(String msg, Throwable cause) { + public AuthenticationException(@Nullable String msg, Throwable cause) { super(msg, cause); } @@ -61,7 +63,7 @@ public abstract class AuthenticationException extends RuntimeException { * debugging * @since 6.5 */ - public Authentication getAuthenticationRequest() { + public @Nullable Authentication getAuthenticationRequest() { return this.authenticationRequest; } diff --git a/core/src/main/java/org/springframework/security/core/ComparableVersion.java b/core/src/main/java/org/springframework/security/core/ComparableVersion.java index 88708cecd4..861549b2af 100644 --- a/core/src/main/java/org/springframework/security/core/ComparableVersion.java +++ b/core/src/main/java/org/springframework/security/core/ComparableVersion.java @@ -26,6 +26,8 @@ import java.util.List; import java.util.Locale; import java.util.Properties; +import org.jspecify.annotations.Nullable; + /** *

* Generic implementation of version comparison. @@ -72,7 +74,8 @@ class ComparableVersion implements Comparable { private String value; - private String canonical; + @SuppressWarnings("NullAway.Init") + private @Nullable String canonical; private ListItem items; @@ -88,7 +91,7 @@ class ComparableVersion implements Comparable { int LIST_ITEM = 2; - int compareTo(Item item); + int compareTo(@Nullable Item item); int getType(); @@ -125,7 +128,7 @@ class ComparableVersion implements Comparable { } @Override - public int compareTo(Item item) { + public int compareTo(@Nullable Item item) { if (item == null) { return (value == 0) ? 0 : 1; // 1.0 == 1, 1.1 > 1 } @@ -189,7 +192,7 @@ class ComparableVersion implements Comparable { } @Override - public int compareTo(Item item) { + public int compareTo(@Nullable Item item) { if (item == null) { return (value == 0) ? 0 : 1; // 1.0 == 1, 1.1 > 1 } @@ -253,7 +256,7 @@ class ComparableVersion implements Comparable { } @Override - public int compareTo(Item item) { + public int compareTo(@Nullable Item item) { if (item == null) { return BigInteger.ZERO.equals(value) ? 0 : 1; // 1.0 == 1, 1.1 > 1 } @@ -361,7 +364,7 @@ class ComparableVersion implements Comparable { } @Override - public int compareTo(Item item) { + public int compareTo(@Nullable Item item) { if (item == null) { // 1-rc < 1, 1-ga > 1 return comparableQualifier(value).compareTo(RELEASE_VERSION_INDEX); @@ -433,7 +436,7 @@ class ComparableVersion implements Comparable { } @Override - public int compareTo(Item item) { + public int compareTo(@Nullable Item item) { if (item == null) { if (isEmpty()) { return 0; // 1-0 = 1- (normalize) = 1 diff --git a/core/src/main/java/org/springframework/security/core/SpringSecurityCoreVersion.java b/core/src/main/java/org/springframework/security/core/SpringSecurityCoreVersion.java index 186b80a89b..a1e68d3082 100644 --- a/core/src/main/java/org/springframework/security/core/SpringSecurityCoreVersion.java +++ b/core/src/main/java/org/springframework/security/core/SpringSecurityCoreVersion.java @@ -22,8 +22,10 @@ import java.util.Properties; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.Nullable; import org.springframework.core.SpringVersion; +import org.springframework.util.Assert; /** * Internal class used for checking version compatibility in a deployed application. @@ -42,7 +44,7 @@ public final class SpringSecurityCoreVersion { */ public static final long SERIAL_VERSION_UID = 620L; - static final String MIN_SPRING_VERSION = getSpringVersion(); + static final @Nullable String MIN_SPRING_VERSION = getSpringVersion(); static { performVersionChecks(); @@ -59,7 +61,7 @@ public final class SpringSecurityCoreVersion { * Perform version checks with specific min Spring Version * @param minSpringVersion */ - private static void performVersionChecks(String minSpringVersion) { + private static void performVersionChecks(@Nullable String minSpringVersion) { if (minSpringVersion == null) { return; } @@ -69,6 +71,8 @@ public final class SpringSecurityCoreVersion { if (disableChecks(springVersion, version)) { return; } + // should be disabled if springVersion is null + Assert.notNull(springVersion, "springVersion cannot be null"); logger.info("You are running with Spring Security Core " + version); if (new ComparableVersion(springVersion).compareTo(new ComparableVersion(minSpringVersion)) < 0) { logger.warn("**** You are advised to use Spring " + minSpringVersion @@ -76,7 +80,7 @@ public final class SpringSecurityCoreVersion { } } - public static String getVersion() { + public static @Nullable String getVersion() { Package pkg = SpringSecurityCoreVersion.class.getPackage(); return (pkg != null) ? pkg.getImplementationVersion() : null; } @@ -88,7 +92,7 @@ public final class SpringSecurityCoreVersion { * @param springSecurityVersion * @return */ - private static boolean disableChecks(String springVersion, String springSecurityVersion) { + private static boolean disableChecks(@Nullable String springVersion, @Nullable String springSecurityVersion) { if (springVersion == null || springVersion.equals(springSecurityVersion)) { return true; } @@ -99,7 +103,7 @@ public final class SpringSecurityCoreVersion { * Loads the spring version or null if it cannot be found. * @return */ - private static String getSpringVersion() { + private static @Nullable String getSpringVersion() { Properties properties = new Properties(); try (InputStream is = SpringSecurityCoreVersion.class.getClassLoader() .getResourceAsStream("META-INF/spring-security.versions")) { diff --git a/core/src/main/java/org/springframework/security/core/annotation/AbstractSecurityAnnotationScanner.java b/core/src/main/java/org/springframework/security/core/annotation/AbstractSecurityAnnotationScanner.java index e192405eb1..610678a059 100644 --- a/core/src/main/java/org/springframework/security/core/annotation/AbstractSecurityAnnotationScanner.java +++ b/core/src/main/java/org/springframework/security/core/annotation/AbstractSecurityAnnotationScanner.java @@ -21,8 +21,9 @@ import java.lang.reflect.AnnotatedElement; import java.lang.reflect.Method; import java.lang.reflect.Parameter; +import org.jspecify.annotations.Nullable; + import org.springframework.core.annotation.MergedAnnotation; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** @@ -37,9 +38,8 @@ abstract class AbstractSecurityAnnotationScanner implement /** * {@inheritDoc} **/ - @Nullable @Override - public A scan(Method method, Class targetClass) { + public @Nullable A scan(Method method, Class targetClass) { Assert.notNull(targetClass, "targetClass cannot be null"); MergedAnnotation annotation = merge(method, targetClass); if (annotation == null) { @@ -51,9 +51,8 @@ abstract class AbstractSecurityAnnotationScanner implement /** * {@inheritDoc} **/ - @Nullable @Override - public A scan(Parameter parameter) { + public @Nullable A scan(Parameter parameter) { MergedAnnotation annotation = merge(parameter, null); if (annotation == null) { return null; @@ -61,6 +60,6 @@ abstract class AbstractSecurityAnnotationScanner implement return annotation.synthesize(); } - abstract MergedAnnotation merge(AnnotatedElement element, Class targetClass); + abstract @Nullable MergedAnnotation merge(AnnotatedElement element, @Nullable Class targetClass); } diff --git a/core/src/main/java/org/springframework/security/core/annotation/ExpressionTemplateSecurityAnnotationScanner.java b/core/src/main/java/org/springframework/security/core/annotation/ExpressionTemplateSecurityAnnotationScanner.java index 7ee6e58d74..388ddbef0a 100644 --- a/core/src/main/java/org/springframework/security/core/annotation/ExpressionTemplateSecurityAnnotationScanner.java +++ b/core/src/main/java/org/springframework/security/core/annotation/ExpressionTemplateSecurityAnnotationScanner.java @@ -25,6 +25,8 @@ import java.util.HashMap; import java.util.Map; import java.util.Set; +import org.jspecify.annotations.Nullable; + import org.springframework.core.annotation.MergedAnnotation; import org.springframework.core.convert.TypeDescriptor; import org.springframework.core.convert.converter.GenericConverter; @@ -89,7 +91,7 @@ final class ExpressionTemplateSecurityAnnotationScanner } @Override - MergedAnnotation merge(AnnotatedElement element, Class targetClass) { + @Nullable MergedAnnotation merge(AnnotatedElement element, @Nullable Class targetClass) { if (element instanceof Parameter parameter) { MergedAnnotation annotation = this.unique.merge(parameter, targetClass); if (annotation == null) { @@ -154,7 +156,7 @@ final class ExpressionTemplateSecurityAnnotationScanner } @Override - public Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType) { + public @Nullable Object convert(@Nullable Object source, TypeDescriptor sourceType, TypeDescriptor targetType) { return (source != null) ? source.toString() : null; } diff --git a/core/src/main/java/org/springframework/security/core/annotation/SecurityAnnotationScanner.java b/core/src/main/java/org/springframework/security/core/annotation/SecurityAnnotationScanner.java index c6b2535aae..e0cc294080 100644 --- a/core/src/main/java/org/springframework/security/core/annotation/SecurityAnnotationScanner.java +++ b/core/src/main/java/org/springframework/security/core/annotation/SecurityAnnotationScanner.java @@ -20,7 +20,7 @@ import java.lang.annotation.Annotation; import java.lang.reflect.Method; import java.lang.reflect.Parameter; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * An interface to scan for and synthesize an annotation on a type, method, or method @@ -62,8 +62,7 @@ public interface SecurityAnnotationScanner { * @param targetClass the target class for the method * @return the synthesized annotation or {@code null} if not found */ - @Nullable - A scan(Method method, Class targetClass); + @Nullable A scan(Method method, Class targetClass); /** * Scan for an annotation of type {@code A}, starting from the given method parameter. @@ -78,7 +77,6 @@ public interface SecurityAnnotationScanner { * @param element the element to search * @return the synthesized annotation or {@code null} if not found */ - @Nullable - A scan(Parameter parameter); + @Nullable A scan(Parameter parameter); } diff --git a/core/src/main/java/org/springframework/security/core/annotation/UniqueSecurityAnnotationScanner.java b/core/src/main/java/org/springframework/security/core/annotation/UniqueSecurityAnnotationScanner.java index f4f5b22e51..be9d8421ec 100644 --- a/core/src/main/java/org/springframework/security/core/annotation/UniqueSecurityAnnotationScanner.java +++ b/core/src/main/java/org/springframework/security/core/annotation/UniqueSecurityAnnotationScanner.java @@ -31,6 +31,8 @@ import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; +import org.jspecify.annotations.Nullable; + import org.springframework.core.MethodClassKey; import org.springframework.core.ResolvableType; import org.springframework.core.annotation.AnnotationConfigurationException; @@ -109,7 +111,7 @@ final class UniqueSecurityAnnotationScanner extends Abstra } @Override - MergedAnnotation merge(AnnotatedElement element, Class targetClass) { + MergedAnnotation merge(AnnotatedElement element, @Nullable Class targetClass) { if (element instanceof Parameter parameter) { return this.uniqueParameterAnnotationCache.computeIfAbsent(parameter, (p) -> { List> annotations = findParameterAnnotations(p); @@ -125,7 +127,8 @@ final class UniqueSecurityAnnotationScanner extends Abstra throw new AnnotationConfigurationException("Unsupported element of type " + element.getClass()); } - private MergedAnnotation requireUnique(AnnotatedElement element, List> annotations) { + private @Nullable MergedAnnotation requireUnique(AnnotatedElement element, + List> annotations) { return switch (annotations.size()) { case 0 -> null; case 1 -> annotations.get(0); @@ -193,7 +196,7 @@ final class UniqueSecurityAnnotationScanner extends Abstra return Collections.emptyList(); } - private List> findMethodAnnotations(Method method, Class targetClass) { + private List> findMethodAnnotations(Method method, @Nullable Class targetClass) { // The method may be on an interface, but we need attributes from the target // class. // If the target class is null, the method will be unchanged. @@ -265,7 +268,7 @@ final class UniqueSecurityAnnotationScanner extends Abstra .toList(); } - private static Method findMethod(Method method, Class targetClass) { + private static @Nullable Method findMethod(Method method, Class targetClass) { for (Method candidate : targetClass.getDeclaredMethods()) { if (candidate.equals(method)) { return candidate; diff --git a/core/src/main/java/org/springframework/security/core/annotation/package-info.java b/core/src/main/java/org/springframework/security/core/annotation/package-info.java new file mode 100644 index 0000000000..f6b2c89e1d --- /dev/null +++ b/core/src/main/java/org/springframework/security/core/annotation/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright 2002-2025 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.core.annotation; + +import org.jspecify.annotations.NullMarked; diff --git a/core/src/main/java/org/springframework/security/core/authority/mapping/MapBasedAttributes2GrantedAuthoritiesMapper.java b/core/src/main/java/org/springframework/security/core/authority/mapping/MapBasedAttributes2GrantedAuthoritiesMapper.java index 1bca8eb7e3..ad71ad5cb2 100755 --- a/core/src/main/java/org/springframework/security/core/authority/mapping/MapBasedAttributes2GrantedAuthoritiesMapper.java +++ b/core/src/main/java/org/springframework/security/core/authority/mapping/MapBasedAttributes2GrantedAuthoritiesMapper.java @@ -20,6 +20,7 @@ import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; @@ -42,11 +43,11 @@ import org.springframework.util.StringUtils; public class MapBasedAttributes2GrantedAuthoritiesMapper implements Attributes2GrantedAuthoritiesMapper, MappableAttributesRetriever, InitializingBean { - private Map> attributes2grantedAuthoritiesMap = null; + private Map> attributes2grantedAuthoritiesMap = new HashMap<>(); private String stringSeparator = ","; - private Set mappableAttributes = null; + private Set mappableAttributes = new HashSet<>(); @Override public void afterPropertiesSet() { @@ -181,6 +182,7 @@ public class MapBasedAttributes2GrantedAuthoritiesMapper * @param stringSeparator The stringSeparator to set. */ public void setStringSeparator(String stringSeparator) { + Assert.notNull(stringSeparator, "stringSeparator cannot be null"); this.stringSeparator = stringSeparator; } diff --git a/core/src/main/java/org/springframework/security/core/authority/mapping/SimpleAuthorityMapper.java b/core/src/main/java/org/springframework/security/core/authority/mapping/SimpleAuthorityMapper.java index 18af306e72..10c1e9051e 100644 --- a/core/src/main/java/org/springframework/security/core/authority/mapping/SimpleAuthorityMapper.java +++ b/core/src/main/java/org/springframework/security/core/authority/mapping/SimpleAuthorityMapper.java @@ -21,6 +21,8 @@ import java.util.HashSet; import java.util.Locale; import java.util.Set; +import org.jspecify.annotations.Nullable; + import org.springframework.beans.factory.InitializingBean; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority; @@ -36,7 +38,7 @@ import org.springframework.util.Assert; */ public final class SimpleAuthorityMapper implements GrantedAuthoritiesMapper, InitializingBean { - private GrantedAuthority defaultAuthority; + private @Nullable GrantedAuthority defaultAuthority; private String prefix = "ROLE_"; diff --git a/core/src/main/java/org/springframework/security/core/authority/mapping/SimpleMappableAttributesRetriever.java b/core/src/main/java/org/springframework/security/core/authority/mapping/SimpleMappableAttributesRetriever.java index 057cc22d9e..e65d175ce3 100755 --- a/core/src/main/java/org/springframework/security/core/authority/mapping/SimpleMappableAttributesRetriever.java +++ b/core/src/main/java/org/springframework/security/core/authority/mapping/SimpleMappableAttributesRetriever.java @@ -29,7 +29,7 @@ import java.util.Set; */ public class SimpleMappableAttributesRetriever implements MappableAttributesRetriever { - private Set mappableAttributes = null; + private Set mappableAttributes = new HashSet<>(); @Override public Set getMappableAttributes() { diff --git a/core/src/main/java/org/springframework/security/core/authority/mapping/package-info.java b/core/src/main/java/org/springframework/security/core/authority/mapping/package-info.java index 48f121691b..c2767b8fd4 100644 --- a/core/src/main/java/org/springframework/security/core/authority/mapping/package-info.java +++ b/core/src/main/java/org/springframework/security/core/authority/mapping/package-info.java @@ -21,4 +21,7 @@ * Provides a layer of indirection between a security data repository and the logical * authorities required within an application. */ +@NullMarked package org.springframework.security.core.authority.mapping; + +import org.jspecify.annotations.NullMarked; diff --git a/core/src/main/java/org/springframework/security/core/authority/package-info.java b/core/src/main/java/org/springframework/security/core/authority/package-info.java index b47cd17e4b..6e0397a2cf 100644 --- a/core/src/main/java/org/springframework/security/core/authority/package-info.java +++ b/core/src/main/java/org/springframework/security/core/authority/package-info.java @@ -17,4 +17,7 @@ /** * The default implementation of the {@code GrantedAuthority} interface. */ +@NullMarked package org.springframework.security.core.authority; + +import org.jspecify.annotations.NullMarked; diff --git a/core/src/main/java/org/springframework/security/core/context/GlobalSecurityContextHolderStrategy.java b/core/src/main/java/org/springframework/security/core/context/GlobalSecurityContextHolderStrategy.java index d8367c4ebd..ded1a611b2 100644 --- a/core/src/main/java/org/springframework/security/core/context/GlobalSecurityContextHolderStrategy.java +++ b/core/src/main/java/org/springframework/security/core/context/GlobalSecurityContextHolderStrategy.java @@ -16,6 +16,8 @@ package org.springframework.security.core.context; +import org.jspecify.annotations.Nullable; + import org.springframework.util.Assert; /** @@ -29,7 +31,7 @@ import org.springframework.util.Assert; */ final class GlobalSecurityContextHolderStrategy implements SecurityContextHolderStrategy { - private static SecurityContext contextHolder; + private static @Nullable SecurityContext contextHolder; @Override public void clearContext() { diff --git a/core/src/main/java/org/springframework/security/core/context/ObservationSecurityContextChangedListener.java b/core/src/main/java/org/springframework/security/core/context/ObservationSecurityContextChangedListener.java index cc0807bb88..0a0c5f9425 100644 --- a/core/src/main/java/org/springframework/security/core/context/ObservationSecurityContextChangedListener.java +++ b/core/src/main/java/org/springframework/security/core/context/ObservationSecurityContextChangedListener.java @@ -18,6 +18,7 @@ package org.springframework.security.core.context; import io.micrometer.observation.Observation; import io.micrometer.observation.ObservationRegistry; +import org.jspecify.annotations.Nullable; import org.springframework.security.core.Authentication; @@ -54,6 +55,7 @@ public final class ObservationSecurityContextChangedListener implements Security * {@inheritDoc} */ @Override + @SuppressWarnings("NullAway") // Dataflow analysis limitation public void securityContextChanged(SecurityContextChangedEvent event) { Observation observation = this.registry.getCurrentObservation(); if (observation == null) { @@ -69,11 +71,15 @@ public final class ObservationSecurityContextChangedListener implements Security return; } if (oldAuthentication == null) { + // NOTE: NullAway thinks that newAuthentication can be null, but it cannot due + // to previous check observation.event(Observation.Event.of(SECURITY_CONTEXT_CREATED, "%s [%s]") .format(SECURITY_CONTEXT_CREATED, newAuthentication.getClass().getSimpleName())); return; } if (newAuthentication == null) { + // NOTE: NullAway thinks that oldAuthentication can be null, but it cannot due + // to previous check observation.event(Observation.Event.of(SECURITY_CONTEXT_CLEARED, "%s [%s]") .format(SECURITY_CONTEXT_CLEARED, oldAuthentication.getClass().getSimpleName())); return; @@ -86,7 +92,7 @@ public final class ObservationSecurityContextChangedListener implements Security newAuthentication.getClass().getSimpleName())); } - private static Authentication getAuthentication(SecurityContext context) { + private static @Nullable Authentication getAuthentication(SecurityContext context) { if (context == null) { return null; } diff --git a/core/src/main/java/org/springframework/security/core/context/SecurityContext.java b/core/src/main/java/org/springframework/security/core/context/SecurityContext.java index bfc40672fd..91d70faf01 100644 --- a/core/src/main/java/org/springframework/security/core/context/SecurityContext.java +++ b/core/src/main/java/org/springframework/security/core/context/SecurityContext.java @@ -18,6 +18,8 @@ package org.springframework.security.core.context; import java.io.Serializable; +import org.jspecify.annotations.Nullable; + import org.springframework.security.core.Authentication; /** @@ -37,7 +39,7 @@ public interface SecurityContext extends Serializable { * @return the Authentication or null if no authentication * information is available */ - Authentication getAuthentication(); + @Nullable Authentication getAuthentication(); /** * Changes the currently authenticated principal, or removes the authentication @@ -45,6 +47,6 @@ public interface SecurityContext extends Serializable { * @param authentication the new Authentication token, or * null if no further authentication information should be stored */ - void setAuthentication(Authentication authentication); + void setAuthentication(@Nullable Authentication authentication); } diff --git a/core/src/main/java/org/springframework/security/core/context/SecurityContextHolder.java b/core/src/main/java/org/springframework/security/core/context/SecurityContextHolder.java index de593d1fc0..394b5d7d9c 100644 --- a/core/src/main/java/org/springframework/security/core/context/SecurityContextHolder.java +++ b/core/src/main/java/org/springframework/security/core/context/SecurityContextHolder.java @@ -64,7 +64,7 @@ public class SecurityContextHolder { private static String strategyName = System.getProperty(SYSTEM_PROPERTY); - private static SecurityContextHolderStrategy strategy; + private static SecurityContextHolderStrategy strategy = new ThreadLocalSecurityContextHolderStrategy(); private static int initializeCount = 0; diff --git a/core/src/main/java/org/springframework/security/core/context/SecurityContextHolderThreadLocalAccessor.java b/core/src/main/java/org/springframework/security/core/context/SecurityContextHolderThreadLocalAccessor.java index 79b817d73f..8145b32416 100644 --- a/core/src/main/java/org/springframework/security/core/context/SecurityContextHolderThreadLocalAccessor.java +++ b/core/src/main/java/org/springframework/security/core/context/SecurityContextHolderThreadLocalAccessor.java @@ -17,6 +17,7 @@ package org.springframework.security.core.context; import io.micrometer.context.ThreadLocalAccessor; +import org.jspecify.annotations.Nullable; import org.springframework.util.Assert; @@ -42,7 +43,7 @@ public final class SecurityContextHolderThreadLocalAccessor implements ThreadLoc } @Override - public SecurityContext getValue() { + public @Nullable SecurityContext getValue() { SecurityContext securityContext = SecurityContextHolder.getContext(); SecurityContext emptyContext = SecurityContextHolder.createEmptyContext(); diff --git a/core/src/main/java/org/springframework/security/core/context/SecurityContextImpl.java b/core/src/main/java/org/springframework/security/core/context/SecurityContextImpl.java index c83bba8741..b33961aa81 100644 --- a/core/src/main/java/org/springframework/security/core/context/SecurityContextImpl.java +++ b/core/src/main/java/org/springframework/security/core/context/SecurityContextImpl.java @@ -16,6 +16,8 @@ package org.springframework.security.core.context; +import org.jspecify.annotations.Nullable; + import org.springframework.security.core.Authentication; import org.springframework.security.core.SpringSecurityCoreVersion; import org.springframework.util.ObjectUtils; @@ -31,7 +33,7 @@ public class SecurityContextImpl implements SecurityContext { private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID; - private Authentication authentication; + private @Nullable Authentication authentication; public SecurityContextImpl() { } @@ -55,7 +57,7 @@ public class SecurityContextImpl implements SecurityContext { } @Override - public Authentication getAuthentication() { + public @Nullable Authentication getAuthentication() { return this.authentication; } @@ -65,7 +67,7 @@ public class SecurityContextImpl implements SecurityContext { } @Override - public void setAuthentication(Authentication authentication) { + public void setAuthentication(@Nullable Authentication authentication) { this.authentication = authentication; } diff --git a/core/src/main/java/org/springframework/security/core/context/package-info.java b/core/src/main/java/org/springframework/security/core/context/package-info.java index 61009683ca..559555856c 100644 --- a/core/src/main/java/org/springframework/security/core/context/package-info.java +++ b/core/src/main/java/org/springframework/security/core/context/package-info.java @@ -27,4 +27,7 @@ * {@link org.springframework.security.core.context.SecurityContextHolder * SecurityContextHolder}. */ +@NullMarked package org.springframework.security.core.context; + +import org.jspecify.annotations.NullMarked; diff --git a/core/src/main/java/org/springframework/security/core/package-info.java b/core/src/main/java/org/springframework/security/core/package-info.java index b8dd2eaa5a..6767819b7a 100644 --- a/core/src/main/java/org/springframework/security/core/package-info.java +++ b/core/src/main/java/org/springframework/security/core/package-info.java @@ -18,4 +18,7 @@ * Core classes and interfaces related to user authentication and authorization, as well * as the maintenance of a security context. */ +@NullMarked package org.springframework.security.core; + +import org.jspecify.annotations.NullMarked; diff --git a/core/src/main/java/org/springframework/security/core/parameters/AnnotationParameterNameDiscoverer.java b/core/src/main/java/org/springframework/security/core/parameters/AnnotationParameterNameDiscoverer.java index 8f1fbc852b..d8d224adff 100644 --- a/core/src/main/java/org/springframework/security/core/parameters/AnnotationParameterNameDiscoverer.java +++ b/core/src/main/java/org/springframework/security/core/parameters/AnnotationParameterNameDiscoverer.java @@ -24,6 +24,8 @@ import java.util.Arrays; import java.util.HashSet; import java.util.Set; +import org.jspecify.annotations.Nullable; + import org.springframework.core.BridgeMethodResolver; import org.springframework.core.ParameterNameDiscoverer; import org.springframework.core.PrioritizedParameterNameDiscoverer; @@ -105,7 +107,7 @@ public class AnnotationParameterNameDiscoverer implements ParameterNameDiscovere } @Override - public String[] getParameterNames(Method method) { + public String @Nullable [] getParameterNames(Method method) { Method originalMethod = BridgeMethodResolver.findBridgedMethod(method); String[] paramNames = lookupParameterNames(METHOD_METHODPARAM_FACTORY, originalMethod); if (paramNames != null) { @@ -123,7 +125,7 @@ public class AnnotationParameterNameDiscoverer implements ParameterNameDiscovere } @Override - public String[] getParameterNames(Constructor constructor) { + public String @Nullable [] getParameterNames(Constructor constructor) { return lookupParameterNames(CONSTRUCTOR_METHODPARAM_FACTORY, constructor); } @@ -134,8 +136,8 @@ public class AnnotationParameterNameDiscoverer implements ParameterNameDiscovere * or Constructor) * @return the parameter names or null */ - private String[] lookupParameterNames(ParameterNameFactory parameterNameFactory, - T t) { + private String @Nullable [] lookupParameterNames( + ParameterNameFactory parameterNameFactory, T t) { Annotation[][] parameterAnnotations = parameterNameFactory.findParameterAnnotations(t); int parameterCount = parameterAnnotations.length; String[] paramNames = new String[parameterCount]; @@ -158,7 +160,7 @@ public class AnnotationParameterNameDiscoverer implements ParameterNameDiscovere * @param parameterAnnotations the {@link Annotation}'s to search. * @return */ - private String findParameterName(Annotation[] parameterAnnotations) { + private @Nullable String findParameterName(Annotation[] parameterAnnotations) { for (Annotation paramAnnotation : parameterAnnotations) { if (this.annotationClassesToUse.contains(paramAnnotation.annotationType().getName())) { return (String) AnnotationUtils.getValue(paramAnnotation, "value"); diff --git a/core/src/main/java/org/springframework/security/core/parameters/package-info.java b/core/src/main/java/org/springframework/security/core/parameters/package-info.java new file mode 100644 index 0000000000..2782314fe7 --- /dev/null +++ b/core/src/main/java/org/springframework/security/core/parameters/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright 2002-2025 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.core.parameters; + +import org.jspecify.annotations.NullMarked; diff --git a/core/src/main/java/org/springframework/security/core/session/SessionRegistry.java b/core/src/main/java/org/springframework/security/core/session/SessionRegistry.java index 107dfaf54c..878a6202f7 100644 --- a/core/src/main/java/org/springframework/security/core/session/SessionRegistry.java +++ b/core/src/main/java/org/springframework/security/core/session/SessionRegistry.java @@ -18,6 +18,8 @@ package org.springframework.security.core.session; import java.util.List; +import org.jspecify.annotations.Nullable; + /** * Maintains a registry of SessionInformation instances. * @@ -49,7 +51,7 @@ public interface SessionRegistry { * @param sessionId to lookup (should never be null) * @return the session information, or null if not found */ - SessionInformation getSessionInformation(String sessionId); + @Nullable SessionInformation getSessionInformation(String sessionId); /** * Updates the given sessionId so its last request time is equal to the diff --git a/core/src/main/java/org/springframework/security/core/session/SessionRegistryImpl.java b/core/src/main/java/org/springframework/security/core/session/SessionRegistryImpl.java index a390931541..df5b86b027 100644 --- a/core/src/main/java/org/springframework/security/core/session/SessionRegistryImpl.java +++ b/core/src/main/java/org/springframework/security/core/session/SessionRegistryImpl.java @@ -28,6 +28,7 @@ import java.util.concurrent.CopyOnWriteArraySet; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.Nullable; import org.springframework.context.ApplicationListener; import org.springframework.core.log.LogMessage; @@ -93,7 +94,7 @@ public class SessionRegistryImpl implements SessionRegistry, ApplicationListener } @Override - public SessionInformation getSessionInformation(String sessionId) { + public @Nullable SessionInformation getSessionInformation(String sessionId) { Assert.hasText(sessionId, "SessionId required as per interface contract"); return this.sessionIds.get(sessionId); } diff --git a/core/src/main/java/org/springframework/security/core/session/package-info.java b/core/src/main/java/org/springframework/security/core/session/package-info.java index bdda791db2..32f318ceca 100644 --- a/core/src/main/java/org/springframework/security/core/session/package-info.java +++ b/core/src/main/java/org/springframework/security/core/session/package-info.java @@ -22,4 +22,7 @@ * core part of the web-based concurrent session control, but the code is not dependent on * any of the servlet APIs. */ +@NullMarked package org.springframework.security.core.session; + +import org.jspecify.annotations.NullMarked; diff --git a/core/src/main/java/org/springframework/security/core/token/KeyBasedPersistenceTokenService.java b/core/src/main/java/org/springframework/security/core/token/KeyBasedPersistenceTokenService.java index 68dc0cc42c..ef13527f0d 100644 --- a/core/src/main/java/org/springframework/security/core/token/KeyBasedPersistenceTokenService.java +++ b/core/src/main/java/org/springframework/security/core/token/KeyBasedPersistenceTokenService.java @@ -19,6 +19,8 @@ package org.springframework.security.core.token; import java.security.SecureRandom; import java.util.Base64; +import org.jspecify.annotations.Nullable; + import org.springframework.beans.factory.InitializingBean; import org.springframework.security.crypto.codec.Hex; import org.springframework.security.crypto.codec.Utf8; @@ -75,10 +77,13 @@ public class KeyBasedPersistenceTokenService implements TokenService, Initializi private int pseudoRandomNumberBytes = 32; + @SuppressWarnings("NullAway.Init") private String serverSecret; + @SuppressWarnings("NullAway.Init") private Integer serverInteger; + @SuppressWarnings("NullAway.Init") private SecureRandom secureRandom; @Override @@ -99,7 +104,7 @@ public class KeyBasedPersistenceTokenService implements TokenService, Initializi } @Override - public Token verifyToken(String key) { + public @Nullable Token verifyToken(String key) { if (key == null || "".equals(key)) { return null; } diff --git a/core/src/main/java/org/springframework/security/core/token/SecureRandomFactoryBean.java b/core/src/main/java/org/springframework/security/core/token/SecureRandomFactoryBean.java index 2e73c5dcf7..6655d1687e 100644 --- a/core/src/main/java/org/springframework/security/core/token/SecureRandomFactoryBean.java +++ b/core/src/main/java/org/springframework/security/core/token/SecureRandomFactoryBean.java @@ -19,6 +19,8 @@ package org.springframework.security.core.token; import java.io.InputStream; import java.security.SecureRandom; +import org.jspecify.annotations.Nullable; + import org.springframework.beans.factory.FactoryBean; import org.springframework.core.io.Resource; import org.springframework.util.Assert; @@ -34,7 +36,7 @@ public class SecureRandomFactoryBean implements FactoryBean { private String algorithm = "SHA1PRNG"; - private Resource seed; + private @Nullable Resource seed; @Override public SecureRandom getObject() throws Exception { diff --git a/core/src/main/java/org/springframework/security/core/token/TokenService.java b/core/src/main/java/org/springframework/security/core/token/TokenService.java index 4c31ebc629..289a32300d 100644 --- a/core/src/main/java/org/springframework/security/core/token/TokenService.java +++ b/core/src/main/java/org/springframework/security/core/token/TokenService.java @@ -16,6 +16,8 @@ package org.springframework.security.core.token; +import org.jspecify.annotations.Nullable; + /** * Provides a mechanism to allocate and rebuild secure, randomised tokens. * @@ -61,6 +63,6 @@ public interface TokenService { * @return the token, or null if the token was not issued by this * TokenService */ - Token verifyToken(String key); + @Nullable Token verifyToken(String key); } diff --git a/core/src/main/java/org/springframework/security/core/token/package-info.java b/core/src/main/java/org/springframework/security/core/token/package-info.java index 87b72c1e23..a9e55559fa 100644 --- a/core/src/main/java/org/springframework/security/core/token/package-info.java +++ b/core/src/main/java/org/springframework/security/core/token/package-info.java @@ -17,4 +17,7 @@ /** * A service for building secure random tokens. */ +@NullMarked package org.springframework.security.core.token; + +import org.jspecify.annotations.NullMarked; diff --git a/core/src/main/java/org/springframework/security/core/userdetails/MapReactiveUserDetailsService.java b/core/src/main/java/org/springframework/security/core/userdetails/MapReactiveUserDetailsService.java index ecb459bc3c..bbfd1c9093 100644 --- a/core/src/main/java/org/springframework/security/core/userdetails/MapReactiveUserDetailsService.java +++ b/core/src/main/java/org/springframework/security/core/userdetails/MapReactiveUserDetailsService.java @@ -22,6 +22,7 @@ import java.util.Locale; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; +import org.jspecify.annotations.Nullable; import reactor.core.publisher.Mono; import org.springframework.util.Assert; @@ -72,7 +73,7 @@ public class MapReactiveUserDetailsService implements ReactiveUserDetailsService } @Override - public Mono updatePassword(UserDetails user, String newPassword) { + public Mono updatePassword(UserDetails user, @Nullable String newPassword) { // @formatter:off return Mono.just(user) .map((userDetails) -> withNewPassword(userDetails, newPassword)) @@ -83,7 +84,7 @@ public class MapReactiveUserDetailsService implements ReactiveUserDetailsService // @formatter:on } - private UserDetails withNewPassword(UserDetails userDetails, String newPassword) { + private UserDetails withNewPassword(UserDetails userDetails, @Nullable String newPassword) { // @formatter:off return User.withUserDetails(userDetails) .password(newPassword) diff --git a/core/src/main/java/org/springframework/security/core/userdetails/ReactiveUserDetailsPasswordService.java b/core/src/main/java/org/springframework/security/core/userdetails/ReactiveUserDetailsPasswordService.java index 784da5d008..b33b88146b 100644 --- a/core/src/main/java/org/springframework/security/core/userdetails/ReactiveUserDetailsPasswordService.java +++ b/core/src/main/java/org/springframework/security/core/userdetails/ReactiveUserDetailsPasswordService.java @@ -16,6 +16,7 @@ package org.springframework.security.core.userdetails; +import org.jspecify.annotations.Nullable; import reactor.core.publisher.Mono; /** @@ -26,6 +27,8 @@ import reactor.core.publisher.Mono; */ public interface ReactiveUserDetailsPasswordService { + ReactiveUserDetailsPasswordService NOOP = (user, newPassword) -> Mono.just(user); + /** * Modify the specified user's password. This should change the user's password in the * persistent user repository (database, LDAP etc). @@ -33,6 +36,6 @@ public interface ReactiveUserDetailsPasswordService { * @param newPassword the password to change to * @return the updated UserDetails with the new password */ - Mono updatePassword(UserDetails user, String newPassword); + Mono updatePassword(UserDetails user, @Nullable String newPassword); } diff --git a/core/src/main/java/org/springframework/security/core/userdetails/User.java b/core/src/main/java/org/springframework/security/core/userdetails/User.java index 461e2b478d..3b9a871ba0 100644 --- a/core/src/main/java/org/springframework/security/core/userdetails/User.java +++ b/core/src/main/java/org/springframework/security/core/userdetails/User.java @@ -30,6 +30,7 @@ import java.util.function.Function; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.Nullable; import org.springframework.security.core.CredentialsContainer; import org.springframework.security.core.GrantedAuthority; @@ -66,7 +67,7 @@ public class User implements UserDetails, CredentialsContainer { private static final Log logger = LogFactory.getLog(User.class); - private String password; + private @Nullable String password; private final String username; @@ -83,7 +84,7 @@ public class User implements UserDetails, CredentialsContainer { /** * Calls the more complex constructor with all boolean arguments set to {@code true}. */ - public User(String username, String password, Collection authorities) { + public User(String username, @Nullable String password, Collection authorities) { this(username, password, true, true, true, true, authorities); } @@ -104,11 +105,10 @@ public class User implements UserDetails, CredentialsContainer { * @throws IllegalArgumentException if a null value was passed either as * a parameter or as an element in the GrantedAuthority collection */ - public User(String username, String password, boolean enabled, boolean accountNonExpired, + public User(String username, @Nullable String password, boolean enabled, boolean accountNonExpired, boolean credentialsNonExpired, boolean accountNonLocked, Collection authorities) { - Assert.isTrue(username != null && !"".equals(username) && password != null, - "Cannot pass null or empty values to constructor"); + Assert.isTrue(username != null && !"".equals(username), "Cannot pass null or empty values to constructor"); this.username = username; this.password = password; this.enabled = enabled; @@ -124,7 +124,7 @@ public class User implements UserDetails, CredentialsContainer { } @Override - public String getPassword() { + public @Nullable String getPassword() { return this.password; } @@ -289,14 +289,17 @@ public class User implements UserDetails, CredentialsContainer { public static UserBuilder withUserDetails(UserDetails userDetails) { // @formatter:off - return withUsername(userDetails.getUsername()) - .password(userDetails.getPassword()) + UserBuilder result = withUsername(userDetails.getUsername()) .accountExpired(!userDetails.isAccountNonExpired()) .accountLocked(!userDetails.isAccountNonLocked()) .authorities(userDetails.getAuthorities()) .credentialsExpired(!userDetails.isCredentialsNonExpired()) .disabled(!userDetails.isEnabled()); // @formatter:on + if (userDetails.getPassword() != null) { + result.password(userDetails.getPassword()); + } + return result; } private static class AuthorityComparator implements Comparator, Serializable { @@ -325,9 +328,9 @@ public class User implements UserDetails, CredentialsContainer { */ public static final class UserBuilder { - private String username; + private @Nullable String username; - private String password; + private @Nullable String password; private List authorities = new ArrayList<>(); @@ -339,7 +342,7 @@ public class User implements UserDetails, CredentialsContainer { private boolean disabled; - private Function passwordEncoder = (password) -> password; + private Function<@Nullable String, @Nullable String> passwordEncoder = (password) -> password; /** * Creates a new instance @@ -361,12 +364,11 @@ public class User implements UserDetails, CredentialsContainer { /** * Populates the password. This attribute is required. - * @param password the password. Cannot be null. + * @param password the password. * @return the {@link UserBuilder} for method chaining (i.e. to populate * additional attributes for this user) */ - public UserBuilder password(String password) { - Assert.notNull(password, "password cannot be null"); + public UserBuilder password(@Nullable String password) { this.password = password; return this; } @@ -378,7 +380,7 @@ public class User implements UserDetails, CredentialsContainer { * @return the {@link UserBuilder} for method chaining (i.e. to populate * additional attributes for this user) */ - public UserBuilder passwordEncoder(Function encoder) { + public UserBuilder passwordEncoder(Function<@Nullable String, @Nullable String> encoder) { Assert.notNull(encoder, "encoder cannot be null"); this.passwordEncoder = encoder; return this; @@ -503,7 +505,8 @@ public class User implements UserDetails, CredentialsContainer { } public UserDetails build() { - String encodedPassword = this.passwordEncoder.apply(this.password); + Assert.notNull(this.username, "username cannot be null"); + String encodedPassword = (this.password != null) ? this.passwordEncoder.apply(this.password) : null; return new User(this.username, encodedPassword, !this.disabled, !this.accountExpired, !this.credentialsExpired, !this.accountLocked, this.authorities); } diff --git a/core/src/main/java/org/springframework/security/core/userdetails/UserCache.java b/core/src/main/java/org/springframework/security/core/userdetails/UserCache.java index d352a396b1..76f2b72f88 100644 --- a/core/src/main/java/org/springframework/security/core/userdetails/UserCache.java +++ b/core/src/main/java/org/springframework/security/core/userdetails/UserCache.java @@ -16,6 +16,8 @@ package org.springframework.security.core.userdetails; +import org.jspecify.annotations.Nullable; + /** * Provides a cache of {@link UserDetails} objects. * @@ -43,7 +45,7 @@ public interface UserCache { * @return the populated UserDetails or null if the user * could not be found or if the cache entry has expired */ - UserDetails getUserFromCache(String username); + @Nullable UserDetails getUserFromCache(String username); /** * Places a {@link UserDetails} in the cache. The username is the key diff --git a/core/src/main/java/org/springframework/security/core/userdetails/UserDetails.java b/core/src/main/java/org/springframework/security/core/userdetails/UserDetails.java index bab08c5a2a..a80523f157 100644 --- a/core/src/main/java/org/springframework/security/core/userdetails/UserDetails.java +++ b/core/src/main/java/org/springframework/security/core/userdetails/UserDetails.java @@ -19,6 +19,8 @@ package org.springframework.security.core.userdetails; import java.io.Serializable; import java.util.Collection; +import org.jspecify.annotations.Nullable; + import org.springframework.security.core.Authentication; import org.springframework.security.core.GrantedAuthority; @@ -49,10 +51,11 @@ public interface UserDetails extends Serializable { Collection getAuthorities(); /** - * Returns the password used to authenticate the user. + * Returns the password used to authenticate the user. Can be null if the user has not + * specified a password (e.g. the user Passkeys instead). * @return the password */ - String getPassword(); + @Nullable String getPassword(); /** * Returns the username used to authenticate the user. Cannot return diff --git a/core/src/main/java/org/springframework/security/core/userdetails/UserDetailsByNameServiceWrapper.java b/core/src/main/java/org/springframework/security/core/userdetails/UserDetailsByNameServiceWrapper.java index f116f69104..6a589be4f9 100755 --- a/core/src/main/java/org/springframework/security/core/userdetails/UserDetailsByNameServiceWrapper.java +++ b/core/src/main/java/org/springframework/security/core/userdetails/UserDetailsByNameServiceWrapper.java @@ -32,7 +32,8 @@ import org.springframework.util.Assert; public class UserDetailsByNameServiceWrapper implements AuthenticationUserDetailsService, InitializingBean { - private UserDetailsService userDetailsService = null; + @SuppressWarnings("NullAway.Init") + private UserDetailsService userDetailsService; /** * Constructs an empty wrapper for compatibility with Spring Security 2.0.x's method diff --git a/core/src/main/java/org/springframework/security/core/userdetails/UserDetailsPasswordService.java b/core/src/main/java/org/springframework/security/core/userdetails/UserDetailsPasswordService.java index ed85e088aa..c25a2c0617 100644 --- a/core/src/main/java/org/springframework/security/core/userdetails/UserDetailsPasswordService.java +++ b/core/src/main/java/org/springframework/security/core/userdetails/UserDetailsPasswordService.java @@ -16,6 +16,8 @@ package org.springframework.security.core.userdetails; +import org.jspecify.annotations.Nullable; + /** * An API for changing a {@link UserDetails} password. * @@ -24,6 +26,8 @@ package org.springframework.security.core.userdetails; */ public interface UserDetailsPasswordService { + UserDetailsPasswordService NOOP = (user, newPassword) -> user; + /** * Modify the specified user's password. This should change the user's password in the * persistent user repository (database, LDAP etc). @@ -32,6 +36,6 @@ public interface UserDetailsPasswordService { * {@code PasswordEncoder} * @return the updated UserDetails with the new password */ - UserDetails updatePassword(UserDetails user, String newPassword); + UserDetails updatePassword(UserDetails user, @Nullable String newPassword); } diff --git a/core/src/main/java/org/springframework/security/core/userdetails/UsernameNotFoundException.java b/core/src/main/java/org/springframework/security/core/userdetails/UsernameNotFoundException.java index eff0e13adb..76422593fb 100644 --- a/core/src/main/java/org/springframework/security/core/userdetails/UsernameNotFoundException.java +++ b/core/src/main/java/org/springframework/security/core/userdetails/UsernameNotFoundException.java @@ -18,6 +18,8 @@ package org.springframework.security.core.userdetails; import java.io.Serial; +import org.jspecify.annotations.Nullable; + import org.springframework.security.core.AuthenticationException; /** @@ -31,7 +33,9 @@ public class UsernameNotFoundException extends AuthenticationException { @Serial private static final long serialVersionUID = 1410688585992297006L; - private final String name; + private static final String DEFAULT_USER_NOT_FOUND_MESSAGE = "user not found"; + + private final @Nullable String name; /** * Constructs a UsernameNotFoundException with the specified message. @@ -53,6 +57,11 @@ public class UsernameNotFoundException extends AuthenticationException { this.name = null; } + private UsernameNotFoundException(String msg, String name) { + super(msg); + this.name = name; + } + private UsernameNotFoundException(String msg, String name, Throwable cause) { super(msg, cause); this.name = name; @@ -65,7 +74,7 @@ public class UsernameNotFoundException extends AuthenticationException { * @since 7.0 */ public static UsernameNotFoundException fromUsername(String username) { - return fromUsername(username, null); + return new UsernameNotFoundException(DEFAULT_USER_NOT_FOUND_MESSAGE, username); } /** @@ -76,7 +85,7 @@ public class UsernameNotFoundException extends AuthenticationException { * @since 7.0 */ public static UsernameNotFoundException fromUsername(String username, Throwable cause) { - return new UsernameNotFoundException("user not found", username, cause); + return new UsernameNotFoundException(DEFAULT_USER_NOT_FOUND_MESSAGE, username, cause); } /** @@ -84,7 +93,7 @@ public class UsernameNotFoundException extends AuthenticationException { * @return the username * @since 7.0 */ - public String getName() { + public @Nullable String getName() { return this.name; } diff --git a/core/src/main/java/org/springframework/security/core/userdetails/cache/NullUserCache.java b/core/src/main/java/org/springframework/security/core/userdetails/cache/NullUserCache.java index d831685bd1..7772a65c9f 100644 --- a/core/src/main/java/org/springframework/security/core/userdetails/cache/NullUserCache.java +++ b/core/src/main/java/org/springframework/security/core/userdetails/cache/NullUserCache.java @@ -16,6 +16,8 @@ package org.springframework.security.core.userdetails.cache; +import org.jspecify.annotations.Nullable; + import org.springframework.security.core.userdetails.UserCache; import org.springframework.security.core.userdetails.UserDetails; @@ -27,7 +29,7 @@ import org.springframework.security.core.userdetails.UserDetails; public class NullUserCache implements UserCache { @Override - public UserDetails getUserFromCache(String username) { + public @Nullable UserDetails getUserFromCache(String username) { return null; } diff --git a/core/src/main/java/org/springframework/security/core/userdetails/cache/SpringCacheBasedUserCache.java b/core/src/main/java/org/springframework/security/core/userdetails/cache/SpringCacheBasedUserCache.java index f0cae216e5..144f5e2e96 100644 --- a/core/src/main/java/org/springframework/security/core/userdetails/cache/SpringCacheBasedUserCache.java +++ b/core/src/main/java/org/springframework/security/core/userdetails/cache/SpringCacheBasedUserCache.java @@ -18,6 +18,7 @@ package org.springframework.security.core.userdetails.cache; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.Nullable; import org.springframework.cache.Cache; import org.springframework.core.log.LogMessage; @@ -43,7 +44,7 @@ public class SpringCacheBasedUserCache implements UserCache { } @Override - public UserDetails getUserFromCache(String username) { + public @Nullable UserDetails getUserFromCache(String username) { Cache.ValueWrapper element = (username != null) ? this.cache.get(username) : null; logger.debug(LogMessage.of(() -> "Cache hit: " + (element != null) + "; username: " + username)); return (element != null) ? (UserDetails) element.get() : null; diff --git a/core/src/main/java/org/springframework/security/core/userdetails/cache/package-info.java b/core/src/main/java/org/springframework/security/core/userdetails/cache/package-info.java index 3ce5d7e93b..a406eba2c8 100644 --- a/core/src/main/java/org/springframework/security/core/userdetails/cache/package-info.java +++ b/core/src/main/java/org/springframework/security/core/userdetails/cache/package-info.java @@ -18,4 +18,8 @@ * Implementations of {@link org.springframework.security.core.userdetails.UserCache * UserCache}. */ + +@NullMarked package org.springframework.security.core.userdetails.cache; + +import org.jspecify.annotations.NullMarked; diff --git a/core/src/main/java/org/springframework/security/core/userdetails/jdbc/JdbcDaoImpl.java b/core/src/main/java/org/springframework/security/core/userdetails/jdbc/JdbcDaoImpl.java index 582fddb2f4..47f691ca6a 100644 --- a/core/src/main/java/org/springframework/security/core/userdetails/jdbc/JdbcDaoImpl.java +++ b/core/src/main/java/org/springframework/security/core/userdetails/jdbc/JdbcDaoImpl.java @@ -25,6 +25,7 @@ import org.springframework.context.ApplicationContextException; import org.springframework.context.MessageSource; import org.springframework.context.MessageSourceAware; import org.springframework.context.support.MessageSourceAccessor; +import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.jdbc.core.RowMapper; import org.springframework.jdbc.core.support.JdbcDaoSupport; import org.springframework.security.core.GrantedAuthority; @@ -218,7 +219,7 @@ public class JdbcDaoImpl extends JdbcDaoSupport implements UserDetailsService, M return new User(username1, password, enabled, true, true, true, AuthorityUtils.NO_AUTHORITIES); }; // @formatter:on - return getJdbcTemplate().query(this.usersByUsernameQuery, mapper, username); + return getJdbc().query(this.usersByUsernameQuery, mapper, username); } /** @@ -226,7 +227,7 @@ public class JdbcDaoImpl extends JdbcDaoSupport implements UserDetailsService, M * @return a list of GrantedAuthority objects for the user */ protected List loadUserAuthorities(String username) { - return getJdbcTemplate().query(this.authoritiesByUsernameQuery, (rs, rowNum) -> { + return getJdbc().query(this.authoritiesByUsernameQuery, (rs, rowNum) -> { String roleName = JdbcDaoImpl.this.rolePrefix + rs.getString(2); return new SimpleGrantedAuthority(roleName); }, username); @@ -238,7 +239,7 @@ public class JdbcDaoImpl extends JdbcDaoSupport implements UserDetailsService, M * @return a list of GrantedAuthority objects for the user */ protected List loadGroupAuthorities(String username) { - return getJdbcTemplate().query(this.groupAuthoritiesByUsernameQuery, (rs, rowNum) -> { + return getJdbc().query(this.groupAuthoritiesByUsernameQuery, (rs, rowNum) -> { String roleName = getRolePrefix() + rs.getString(3); return new SimpleGrantedAuthority(roleName); }, username); @@ -374,4 +375,10 @@ public class JdbcDaoImpl extends JdbcDaoSupport implements UserDetailsService, M this.messages = new MessageSourceAccessor(messageSource); } + private JdbcTemplate getJdbc() { + JdbcTemplate template = getJdbcTemplate(); + Assert.notNull(template, "JdbcTemplate cannot be null"); + return template; + } + } diff --git a/core/src/main/java/org/springframework/security/core/userdetails/jdbc/package-info.java b/core/src/main/java/org/springframework/security/core/userdetails/jdbc/package-info.java index 3c9399128f..57e6c8cd75 100644 --- a/core/src/main/java/org/springframework/security/core/userdetails/jdbc/package-info.java +++ b/core/src/main/java/org/springframework/security/core/userdetails/jdbc/package-info.java @@ -18,4 +18,7 @@ * Exposes a JDBC-based authentication repository, implementing * {@code org.springframework.security.core.userdetails.UserDetailsService UserDetailsService}. */ +@NullMarked package org.springframework.security.core.userdetails.jdbc; + +import org.jspecify.annotations.NullMarked; diff --git a/core/src/main/java/org/springframework/security/core/userdetails/memory/UserAttribute.java b/core/src/main/java/org/springframework/security/core/userdetails/memory/UserAttribute.java index 8e3643a223..da502a56ac 100644 --- a/core/src/main/java/org/springframework/security/core/userdetails/memory/UserAttribute.java +++ b/core/src/main/java/org/springframework/security/core/userdetails/memory/UserAttribute.java @@ -20,6 +20,8 @@ import java.util.ArrayList; import java.util.List; import java.util.Vector; +import org.jspecify.annotations.Nullable; + import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority; @@ -33,7 +35,7 @@ public class UserAttribute { private List authorities = new Vector<>(); - private String password; + private @Nullable String password; private boolean enabled = true; @@ -67,7 +69,7 @@ public class UserAttribute { } } - public String getPassword() { + public @Nullable String getPassword() { return this.password; } diff --git a/core/src/main/java/org/springframework/security/core/userdetails/memory/package-info.java b/core/src/main/java/org/springframework/security/core/userdetails/memory/package-info.java index 67b7f1ccbc..7a2faa1152 100644 --- a/core/src/main/java/org/springframework/security/core/userdetails/memory/package-info.java +++ b/core/src/main/java/org/springframework/security/core/userdetails/memory/package-info.java @@ -17,4 +17,7 @@ /** * Exposes an in-memory authentication repository. */ +@NullMarked package org.springframework.security.core.userdetails.memory; + +import org.jspecify.annotations.NullMarked; diff --git a/core/src/main/java/org/springframework/security/core/userdetails/package-info.java b/core/src/main/java/org/springframework/security/core/userdetails/package-info.java index e18dc5218b..0722a84e2b 100644 --- a/core/src/main/java/org/springframework/security/core/userdetails/package-info.java +++ b/core/src/main/java/org/springframework/security/core/userdetails/package-info.java @@ -24,4 +24,7 @@ * {@link org.springframework.security.core.userdetails.AuthenticationUserDetailsService * AuthenticationUserDetailsService}. */ +@NullMarked package org.springframework.security.core.userdetails; + +import org.jspecify.annotations.NullMarked; diff --git a/core/src/main/java/org/springframework/security/jackson2/SecurityJackson2Modules.java b/core/src/main/java/org/springframework/security/jackson2/SecurityJackson2Modules.java index 5db1b2e538..e73be5e224 100644 --- a/core/src/main/java/org/springframework/security/jackson2/SecurityJackson2Modules.java +++ b/core/src/main/java/org/springframework/security/jackson2/SecurityJackson2Modules.java @@ -40,6 +40,7 @@ import com.fasterxml.jackson.databind.jsontype.TypeIdResolver; import com.fasterxml.jackson.databind.jsontype.TypeResolverBuilder; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.Nullable; import org.springframework.core.annotation.AnnotationUtils; import org.springframework.core.log.LogMessage; @@ -126,7 +127,7 @@ public final class SecurityJackson2Modules { } @SuppressWarnings("unchecked") - private static Module loadAndGetInstance(String className, ClassLoader loader) { + private static @Nullable Module loadAndGetInstance(String className, ClassLoader loader) { try { Class securityModule = (Class) ClassUtils.forName(className, loader); if (securityModule != null) { diff --git a/core/src/main/java/org/springframework/security/jackson2/UsernamePasswordAuthenticationTokenDeserializer.java b/core/src/main/java/org/springframework/security/jackson2/UsernamePasswordAuthenticationTokenDeserializer.java index c92bbdbe43..4abb120a75 100644 --- a/core/src/main/java/org/springframework/security/jackson2/UsernamePasswordAuthenticationTokenDeserializer.java +++ b/core/src/main/java/org/springframework/security/jackson2/UsernamePasswordAuthenticationTokenDeserializer.java @@ -29,6 +29,7 @@ import com.fasterxml.jackson.databind.JsonMappingException; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.node.MissingNode; +import org.jspecify.annotations.Nullable; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.GrantedAuthority; @@ -91,7 +92,7 @@ class UsernamePasswordAuthenticationTokenDeserializer extends JsonDeserializer 1) { + Integer usersCount = requireJdbcTemplate().queryForObject(this.userExistsSql, Integer.class, username); + if (usersCount == null || usersCount > 1) { throw new IncorrectResultSizeDataAccessException( "[" + usersCount + "] users found with name '" + username + "', expected 1", 1); } @@ -461,7 +464,11 @@ public class JdbcUserDetailsManager extends JdbcDaoImpl } private int findGroupId(String group) { - return requireJdbcTemplate().queryForObject(this.findGroupIdSql, Integer.class, group); + Integer groupId = requireJdbcTemplate().queryForObject(this.findGroupIdSql, Integer.class, group); + if (groupId == null) { + throw new EmptyResultDataAccessException("Could not find required group '" + group + "'", 1); + } + return groupId; } private JdbcTemplate requireJdbcTemplate() { @@ -630,9 +637,13 @@ public class JdbcUserDetailsManager extends JdbcDaoImpl * @since 7.0 */ @Override - public UserDetails updatePassword(UserDetails user, String newPassword) { + public UserDetails updatePassword(UserDetails user, @Nullable String newPassword) { if (this.enableUpdatePassword) { - UserDetails updated = User.withUserDetails(user).password(newPassword).build(); + // @formatter:off + UserDetails updated = User.withUserDetails(user) + .password(newPassword) + .build(); + // @formatter:on updateUser(updated); return updated; } diff --git a/core/src/main/java/org/springframework/security/provisioning/MutableUser.java b/core/src/main/java/org/springframework/security/provisioning/MutableUser.java index 40b28f611b..675997217d 100644 --- a/core/src/main/java/org/springframework/security/provisioning/MutableUser.java +++ b/core/src/main/java/org/springframework/security/provisioning/MutableUser.java @@ -18,6 +18,8 @@ package org.springframework.security.provisioning; import java.util.Collection; +import org.jspecify.annotations.Nullable; + import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.SpringSecurityCoreVersion; import org.springframework.security.core.userdetails.UserDetails; @@ -30,7 +32,7 @@ class MutableUser implements MutableUserDetails { private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID; - private String password; + private @Nullable String password; private final UserDetails delegate; @@ -40,12 +42,12 @@ class MutableUser implements MutableUserDetails { } @Override - public String getPassword() { + public @Nullable String getPassword() { return this.password; } @Override - public void setPassword(String password) { + public void setPassword(@Nullable String password) { this.password = password; } diff --git a/core/src/main/java/org/springframework/security/provisioning/MutableUserDetails.java b/core/src/main/java/org/springframework/security/provisioning/MutableUserDetails.java index 2a911d668b..49a52bda38 100644 --- a/core/src/main/java/org/springframework/security/provisioning/MutableUserDetails.java +++ b/core/src/main/java/org/springframework/security/provisioning/MutableUserDetails.java @@ -16,6 +16,8 @@ package org.springframework.security.provisioning; +import org.jspecify.annotations.Nullable; + import org.springframework.security.core.userdetails.UserDetails; /** @@ -24,6 +26,6 @@ import org.springframework.security.core.userdetails.UserDetails; */ interface MutableUserDetails extends UserDetails { - void setPassword(String password); + void setPassword(@Nullable String password); } diff --git a/core/src/main/java/org/springframework/security/provisioning/package-info.java b/core/src/main/java/org/springframework/security/provisioning/package-info.java index f661aaa89d..995585cbd1 100644 --- a/core/src/main/java/org/springframework/security/provisioning/package-info.java +++ b/core/src/main/java/org/springframework/security/provisioning/package-info.java @@ -18,4 +18,7 @@ * Contains simple user and authority group account provisioning interfaces together with * a a JDBC-based implementation. */ +@NullMarked package org.springframework.security.provisioning; + +import org.jspecify.annotations.NullMarked; diff --git a/core/src/main/java/org/springframework/security/scheduling/DelegatingSecurityContextSchedulingTaskExecutor.java b/core/src/main/java/org/springframework/security/scheduling/DelegatingSecurityContextSchedulingTaskExecutor.java index 6c517a472e..2943e8c36f 100644 --- a/core/src/main/java/org/springframework/security/scheduling/DelegatingSecurityContextSchedulingTaskExecutor.java +++ b/core/src/main/java/org/springframework/security/scheduling/DelegatingSecurityContextSchedulingTaskExecutor.java @@ -18,6 +18,8 @@ package org.springframework.security.scheduling; import java.util.concurrent.Callable; +import org.jspecify.annotations.Nullable; + import org.springframework.core.task.AsyncTaskExecutor; import org.springframework.scheduling.SchedulingTaskExecutor; import org.springframework.security.concurrent.DelegatingSecurityContextCallable; @@ -46,7 +48,7 @@ public class DelegatingSecurityContextSchedulingTaskExecutor extends DelegatingS * {@link DelegatingSecurityContextCallable} */ public DelegatingSecurityContextSchedulingTaskExecutor(SchedulingTaskExecutor delegateSchedulingTaskExecutor, - SecurityContext securityContext) { + @Nullable SecurityContext securityContext) { super(delegateSchedulingTaskExecutor, securityContext); } diff --git a/core/src/main/java/org/springframework/security/scheduling/DelegatingSecurityContextTaskScheduler.java b/core/src/main/java/org/springframework/security/scheduling/DelegatingSecurityContextTaskScheduler.java index bd14f61788..95192d8112 100644 --- a/core/src/main/java/org/springframework/security/scheduling/DelegatingSecurityContextTaskScheduler.java +++ b/core/src/main/java/org/springframework/security/scheduling/DelegatingSecurityContextTaskScheduler.java @@ -22,6 +22,8 @@ import java.time.Instant; import java.util.Date; import java.util.concurrent.ScheduledFuture; +import org.jspecify.annotations.Nullable; + import org.springframework.core.task.TaskExecutor; import org.springframework.scheduling.TaskScheduler; import org.springframework.scheduling.Trigger; @@ -41,7 +43,7 @@ public class DelegatingSecurityContextTaskScheduler implements TaskScheduler { private final TaskScheduler delegate; - private final SecurityContext securityContext; + private final @Nullable SecurityContext securityContext; /** * Creates a new {@link DelegatingSecurityContextTaskScheduler} that uses the @@ -54,7 +56,7 @@ public class DelegatingSecurityContextTaskScheduler implements TaskScheduler { * @since 5.6 */ public DelegatingSecurityContextTaskScheduler(TaskScheduler delegateTaskScheduler, - SecurityContext securityContext) { + @Nullable SecurityContext securityContext) { Assert.notNull(delegateTaskScheduler, "delegateTaskScheduler cannot be null"); this.delegate = delegateTaskScheduler; this.securityContext = securityContext; @@ -70,7 +72,7 @@ public class DelegatingSecurityContextTaskScheduler implements TaskScheduler { } @Override - public ScheduledFuture schedule(Runnable task, Trigger trigger) { + public @Nullable ScheduledFuture schedule(Runnable task, Trigger trigger) { return this.delegate.schedule(wrap(task), trigger); } diff --git a/core/src/main/java/org/springframework/security/scheduling/package-info.java b/core/src/main/java/org/springframework/security/scheduling/package-info.java new file mode 100644 index 0000000000..8658032e96 --- /dev/null +++ b/core/src/main/java/org/springframework/security/scheduling/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright 2002-2025 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.scheduling; + +import org.jspecify.annotations.NullMarked; diff --git a/core/src/main/java/org/springframework/security/task/DelegatingSecurityContextAsyncTaskExecutor.java b/core/src/main/java/org/springframework/security/task/DelegatingSecurityContextAsyncTaskExecutor.java index 3bbba07a0e..70adcc5f68 100644 --- a/core/src/main/java/org/springframework/security/task/DelegatingSecurityContextAsyncTaskExecutor.java +++ b/core/src/main/java/org/springframework/security/task/DelegatingSecurityContextAsyncTaskExecutor.java @@ -19,6 +19,8 @@ package org.springframework.security.task; import java.util.concurrent.Callable; import java.util.concurrent.Future; +import org.jspecify.annotations.Nullable; + import org.springframework.core.task.AsyncTaskExecutor; import org.springframework.security.concurrent.DelegatingSecurityContextCallable; import org.springframework.security.concurrent.DelegatingSecurityContextRunnable; @@ -45,7 +47,7 @@ public class DelegatingSecurityContextAsyncTaskExecutor extends DelegatingSecuri * {@link DelegatingSecurityContextCallable} */ public DelegatingSecurityContextAsyncTaskExecutor(AsyncTaskExecutor delegateAsyncTaskExecutor, - SecurityContext securityContext) { + @Nullable SecurityContext securityContext) { super(delegateAsyncTaskExecutor, securityContext); } diff --git a/core/src/main/java/org/springframework/security/task/DelegatingSecurityContextTaskExecutor.java b/core/src/main/java/org/springframework/security/task/DelegatingSecurityContextTaskExecutor.java index c051e014b4..d71b8e335e 100644 --- a/core/src/main/java/org/springframework/security/task/DelegatingSecurityContextTaskExecutor.java +++ b/core/src/main/java/org/springframework/security/task/DelegatingSecurityContextTaskExecutor.java @@ -16,6 +16,8 @@ package org.springframework.security.task; +import org.jspecify.annotations.Nullable; + import org.springframework.core.task.TaskExecutor; import org.springframework.security.concurrent.DelegatingSecurityContextExecutor; import org.springframework.security.concurrent.DelegatingSecurityContextRunnable; @@ -39,7 +41,8 @@ public class DelegatingSecurityContextTaskExecutor extends DelegatingSecurityCon * @param securityContext the {@link SecurityContext} to use for each * {@link DelegatingSecurityContextRunnable} */ - public DelegatingSecurityContextTaskExecutor(TaskExecutor delegateTaskExecutor, SecurityContext securityContext) { + public DelegatingSecurityContextTaskExecutor(TaskExecutor delegateTaskExecutor, + @Nullable SecurityContext securityContext) { super(delegateTaskExecutor, securityContext); } diff --git a/core/src/main/java/org/springframework/security/task/package-info.java b/core/src/main/java/org/springframework/security/task/package-info.java new file mode 100644 index 0000000000..a63238f02f --- /dev/null +++ b/core/src/main/java/org/springframework/security/task/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright 2002-2025 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.task; + +import org.jspecify.annotations.NullMarked; diff --git a/core/src/main/java/org/springframework/security/util/FieldUtils.java b/core/src/main/java/org/springframework/security/util/FieldUtils.java index 261d264b0d..c5dcafc1aa 100644 --- a/core/src/main/java/org/springframework/security/util/FieldUtils.java +++ b/core/src/main/java/org/springframework/security/util/FieldUtils.java @@ -18,6 +18,8 @@ package org.springframework.security.util; import java.lang.reflect.Field; +import org.jspecify.annotations.Nullable; + import org.springframework.util.Assert; import org.springframework.util.ReflectionUtils; import org.springframework.util.StringUtils; @@ -78,7 +80,7 @@ public final class FieldUtils { } - public static Object getProtectedFieldValue(String protectedField, Object object) { + public static @Nullable Object getProtectedFieldValue(String protectedField, Object object) { Field field = FieldUtils.getField(object.getClass(), protectedField); try { field.setAccessible(true); diff --git a/core/src/main/java/org/springframework/security/util/InMemoryResource.java b/core/src/main/java/org/springframework/security/util/InMemoryResource.java index 30b3158b6a..79c42ba270 100644 --- a/core/src/main/java/org/springframework/security/util/InMemoryResource.java +++ b/core/src/main/java/org/springframework/security/util/InMemoryResource.java @@ -20,6 +20,8 @@ import java.io.ByteArrayInputStream; import java.io.InputStream; import java.util.Arrays; +import org.jspecify.annotations.Nullable; + import org.springframework.core.io.AbstractResource; import org.springframework.util.Assert; @@ -46,10 +48,10 @@ public class InMemoryResource extends AbstractResource { this(source, null); } - public InMemoryResource(byte[] source, String description) { + public InMemoryResource(byte[] source, @Nullable String description) { Assert.notNull(source, "source cannot be null"); this.source = source; - this.description = description; + this.description = (description != null) ? description : ""; } @Override @@ -63,7 +65,7 @@ public class InMemoryResource extends AbstractResource { } @Override - public boolean equals(Object res) { + public boolean equals(@Nullable Object res) { if (!(res instanceof InMemoryResource)) { return false; } diff --git a/core/src/main/java/org/springframework/security/util/MethodInvocationUtils.java b/core/src/main/java/org/springframework/security/util/MethodInvocationUtils.java index 9c407bfd40..01fd165408 100644 --- a/core/src/main/java/org/springframework/security/util/MethodInvocationUtils.java +++ b/core/src/main/java/org/springframework/security/util/MethodInvocationUtils.java @@ -19,6 +19,7 @@ package org.springframework.security.util; import java.lang.reflect.Method; import org.aopalliance.intercept.MethodInvocation; +import org.jspecify.annotations.Nullable; import org.springframework.aop.framework.Advised; import org.springframework.aop.support.AopUtils; @@ -48,7 +49,7 @@ public final class MethodInvocationUtils { * @return a MethodInvocation, or null if there was a * problem */ - public static MethodInvocation create(Object object, String methodName, Object... args) { + public static @Nullable MethodInvocation create(Object object, String methodName, Object... args) { Assert.notNull(object, "Object required"); Class[] classArgs = null; if (args != null) { @@ -93,7 +94,7 @@ public final class MethodInvocationUtils { * @return a MethodInvocation, or null if there was a * problem */ - public static MethodInvocation createFromClass(Class clazz, String methodName) { + public static @Nullable MethodInvocation createFromClass(Class clazz, String methodName) { MethodInvocation invocation = createFromClass(null, clazz, methodName, null, null); if (invocation == null) { for (Method method : clazz.getDeclaredMethods()) { @@ -120,8 +121,8 @@ public final class MethodInvocationUtils { * @return a MethodInvocation, or null if there was a * problem */ - public static MethodInvocation createFromClass(Object targetObject, Class clazz, String methodName, - Class[] classArgs, Object[] args) { + public static @Nullable MethodInvocation createFromClass(@Nullable Object targetObject, Class clazz, + String methodName, Class @Nullable [] classArgs, Object @Nullable [] args) { Assert.notNull(clazz, "Class required"); Assert.hasText(methodName, "MethodName required"); try { diff --git a/core/src/main/java/org/springframework/security/util/SimpleMethodInvocation.java b/core/src/main/java/org/springframework/security/util/SimpleMethodInvocation.java index 41470b5897..ecbe05a210 100644 --- a/core/src/main/java/org/springframework/security/util/SimpleMethodInvocation.java +++ b/core/src/main/java/org/springframework/security/util/SimpleMethodInvocation.java @@ -20,6 +20,9 @@ import java.lang.reflect.AccessibleObject; import java.lang.reflect.Method; import org.aopalliance.intercept.MethodInvocation; +import org.jspecify.annotations.Nullable; + +import org.springframework.util.Assert; /** * Represents the AOP Alliance MethodInvocation. @@ -28,19 +31,22 @@ import org.aopalliance.intercept.MethodInvocation; */ public class SimpleMethodInvocation implements MethodInvocation { - private Method method; + private @Nullable Method method; private Object[] arguments; - private Object targetObject; + private @Nullable Object targetObject; - public SimpleMethodInvocation(Object targetObject, Method method, Object... arguments) { + // @formatter:off See https://github.com/spring-io/spring-javaformat/issues/ + public SimpleMethodInvocation(@Nullable Object targetObject, Method method, Object @Nullable... arguments) { this.targetObject = targetObject; this.method = method; this.arguments = (arguments != null) ? arguments : new Object[0]; } + // @formatter:on public SimpleMethodInvocation() { + this.arguments = new Object[0]; } @Override @@ -50,6 +56,7 @@ public class SimpleMethodInvocation implements MethodInvocation { @Override public Method getMethod() { + Assert.state(this.method != null, "method cannot be null"); return this.method; } @@ -59,7 +66,7 @@ public class SimpleMethodInvocation implements MethodInvocation { } @Override - public Object getThis() { + public @Nullable Object getThis() { return this.targetObject; } diff --git a/core/src/main/java/org/springframework/security/util/package-info.java b/core/src/main/java/org/springframework/security/util/package-info.java index e8cbd0733d..24f358268a 100644 --- a/core/src/main/java/org/springframework/security/util/package-info.java +++ b/core/src/main/java/org/springframework/security/util/package-info.java @@ -21,4 +21,7 @@ * This package should be standalone - it should not have dependencies on other parts of * the framework, just on external libraries and the JDK. */ +@NullMarked package org.springframework.security.util; + +import org.jspecify.annotations.NullMarked; diff --git a/core/src/test/java/org/springframework/security/access/expression/ExpressionUtilsTests.java b/core/src/test/java/org/springframework/security/access/expression/ExpressionUtilsTests.java new file mode 100644 index 0000000000..5d627ecb2f --- /dev/null +++ b/core/src/test/java/org/springframework/security/access/expression/ExpressionUtilsTests.java @@ -0,0 +1,37 @@ +/* + * Copyright 2002-2025 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.access.expression; + +import org.junit.jupiter.api.Test; + +import org.springframework.expression.Expression; +import org.springframework.expression.spel.standard.SpelExpressionParser; +import org.springframework.expression.spel.support.StandardEvaluationContext; + +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; + +class ExpressionUtilsTests { + + @Test + void evaluateAsBooleanWhenExpressionNullThenFalse() { + SpelExpressionParser parser = new SpelExpressionParser(); + Expression expression = parser.parseExpression("null"); + StandardEvaluationContext context = new StandardEvaluationContext(this); + assertThatIllegalArgumentException().isThrownBy(() -> ExpressionUtils.evaluateAsBoolean(expression, context)); + } + +} diff --git a/core/src/test/java/org/springframework/security/access/expression/method/MethodSecurityEvaluationContextTests.java b/core/src/test/java/org/springframework/security/access/expression/method/MethodSecurityEvaluationContextTests.java index 32601baf57..7dd641e598 100644 --- a/core/src/test/java/org/springframework/security/access/expression/method/MethodSecurityEvaluationContextTests.java +++ b/core/src/test/java/org/springframework/security/access/expression/method/MethodSecurityEvaluationContextTests.java @@ -19,13 +19,13 @@ package org.springframework.security.access.expression.method; import java.lang.reflect.Method; import org.aopalliance.intercept.MethodInvocation; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.core.ParameterNameDiscoverer; -import org.springframework.lang.Nullable; import org.springframework.security.core.Authentication; import org.springframework.util.ReflectionUtils; diff --git a/core/src/test/java/org/springframework/security/authentication/dao/DaoAuthenticationProviderTests.java b/core/src/test/java/org/springframework/security/authentication/dao/DaoAuthenticationProviderTests.java index 2b780fa9b2..1212e5b313 100644 --- a/core/src/test/java/org/springframework/security/authentication/dao/DaoAuthenticationProviderTests.java +++ b/core/src/test/java/org/springframework/security/authentication/dao/DaoAuthenticationProviderTests.java @@ -176,8 +176,8 @@ public class DaoAuthenticationProviderTests { public void testAuthenticateFailsWithInvalidUsernameAndHideUserNotFoundExceptionsWithDefaultOfTrue() { UsernamePasswordAuthenticationToken token = UsernamePasswordAuthenticationToken.unauthenticated("INVALID_USER", "koala"); - assertThat(createProvider(null).isHideUserNotFoundExceptions()).isTrue(); DaoAuthenticationProvider provider = createProvider(new MockUserDetailsServiceUserRod()); + assertThat(provider.isHideUserNotFoundExceptions()).isTrue(); provider.setUserCache(new MockUserCache()); assertThatExceptionOfType(BadCredentialsException.class).isThrownBy(() -> provider.authenticate(token)); } @@ -186,8 +186,8 @@ public class DaoAuthenticationProviderTests { public void testAuthenticateFailsWithInvalidUsernameAndChangePasswordEncoder() { UsernamePasswordAuthenticationToken token = UsernamePasswordAuthenticationToken.unauthenticated("INVALID_USER", "koala"); - assertThat(createProvider(null).isHideUserNotFoundExceptions()).isTrue(); DaoAuthenticationProvider provider = createProvider(new MockUserDetailsServiceUserRod()); + assertThat(provider.isHideUserNotFoundExceptions()).isTrue(); provider.setUserCache(new MockUserCache()); assertThatExceptionOfType(BadCredentialsException.class).isThrownBy(() -> provider.authenticate(token)); provider.setPasswordEncoder(PasswordEncoderFactories.createDelegatingPasswordEncoder()); @@ -319,7 +319,7 @@ public class DaoAuthenticationProviderTests { @Test public void testGettersSetters() { - DaoAuthenticationProvider provider = new DaoAuthenticationProvider(null); + DaoAuthenticationProvider provider = new DaoAuthenticationProvider(new MockUserDetailsServiceUserRod()); provider.setPasswordEncoder(new BCryptPasswordEncoder()); assertThat(provider.getPasswordEncoder().getClass()).isEqualTo(BCryptPasswordEncoder.class); provider.setUserCache(new SpringCacheBasedUserCache(mock(Cache.class))); @@ -350,12 +350,6 @@ public class DaoAuthenticationProviderTests { assertThat(cache.getUserFromCache("rod").getPassword()).isEqualTo("easternLongNeckTurtle"); } - @Test - public void testStartupFailsIfNoAuthenticationDao() throws Exception { - DaoAuthenticationProvider provider = new DaoAuthenticationProvider(null); - assertThatIllegalArgumentException().isThrownBy(provider::afterPropertiesSet); - } - @Test public void testStartupFailsIfNoUserCacheSet() throws Exception { DaoAuthenticationProvider provider = createProvider(new MockUserDetailsServiceUserRod()); @@ -375,7 +369,7 @@ public class DaoAuthenticationProviderTests { @Test public void testSupports() { - DaoAuthenticationProvider provider = createProvider(null); + DaoAuthenticationProvider provider = createProvider(new MockUserDetailsServiceUserRod()); assertThat(provider.supports(UsernamePasswordAuthenticationToken.class)).isTrue(); assertThat(!provider.supports(TestingAuthenticationToken.class)).isTrue(); } @@ -421,7 +415,7 @@ public class DaoAuthenticationProviderTests { @Test public void constructWhenPasswordEncoderProvidedThenSets() { - DaoAuthenticationProvider daoAuthenticationProvider = createProvider(null); + DaoAuthenticationProvider daoAuthenticationProvider = createProvider(new MockUserDetailsServiceUserRod()); daoAuthenticationProvider.setPasswordEncoder(NoOpPasswordEncoder.getInstance()); assertThat(daoAuthenticationProvider.getPasswordEncoder()).isSameAs(NoOpPasswordEncoder.getInstance()); } diff --git a/core/src/test/java/org/springframework/security/authentication/jaas/DefaultJaasAuthenticationProviderTests.java b/core/src/test/java/org/springframework/security/authentication/jaas/DefaultJaasAuthenticationProviderTests.java index 04cf7b85da..da4d1d4bab 100644 --- a/core/src/test/java/org/springframework/security/authentication/jaas/DefaultJaasAuthenticationProviderTests.java +++ b/core/src/test/java/org/springframework/security/authentication/jaas/DefaultJaasAuthenticationProviderTests.java @@ -32,7 +32,6 @@ import org.mockito.ArgumentCaptor; import org.springframework.context.ApplicationEventPublisher; import org.springframework.context.support.ClassPathXmlApplicationContext; -import org.springframework.security.authentication.BadCredentialsException; import org.springframework.security.authentication.TestingAuthenticationToken; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.authentication.jaas.event.JaasAuthenticationFailedEvent; @@ -91,8 +90,7 @@ public class DefaultJaasAuthenticationProviderTests { @Test public void afterPropertiesSetNullAuthorityGranters() throws Exception { - this.provider.setAuthorityGranters(null); - assertThatIllegalArgumentException().isThrownBy(this.provider::afterPropertiesSet); + assertThatIllegalArgumentException().isThrownBy(() -> this.provider.setAuthorityGranters(null)); } @Test @@ -212,10 +210,7 @@ public class DefaultJaasAuthenticationProviderTests { @Test public void publishNullPublisher() { - this.provider.setApplicationEventPublisher(null); - AuthenticationException ae = new BadCredentialsException("Failed to login"); - this.provider.publishFailureEvent(this.token, ae); - this.provider.publishSuccessEvent(this.token); + assertThatIllegalArgumentException().isThrownBy(() -> this.provider.setApplicationEventPublisher(null)); } @Test diff --git a/core/src/test/java/org/springframework/security/core/authority/mapping/MapBasedAttributes2GrantedAuthoritiesMapperTests.java b/core/src/test/java/org/springframework/security/core/authority/mapping/MapBasedAttributes2GrantedAuthoritiesMapperTests.java index 7243467d54..b0a1a19df6 100644 --- a/core/src/test/java/org/springframework/security/core/authority/mapping/MapBasedAttributes2GrantedAuthoritiesMapperTests.java +++ b/core/src/test/java/org/springframework/security/core/authority/mapping/MapBasedAttributes2GrantedAuthoritiesMapperTests.java @@ -36,12 +36,6 @@ import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException @SuppressWarnings("unchecked") public class MapBasedAttributes2GrantedAuthoritiesMapperTests { - @Test - public void testAfterPropertiesSetNoMap() throws Exception { - MapBasedAttributes2GrantedAuthoritiesMapper mapper = new MapBasedAttributes2GrantedAuthoritiesMapper(); - assertThatIllegalArgumentException().isThrownBy(mapper::afterPropertiesSet); - } - @Test public void testAfterPropertiesSetEmptyMap() throws Exception { MapBasedAttributes2GrantedAuthoritiesMapper mapper = new MapBasedAttributes2GrantedAuthoritiesMapper(); diff --git a/core/src/test/java/org/springframework/security/core/context/ObservationSecurityContextChangedListenerTests.java b/core/src/test/java/org/springframework/security/core/context/ObservationSecurityContextChangedListenerTests.java index 21ef56f5f0..8757bcb0ae 100644 --- a/core/src/test/java/org/springframework/security/core/context/ObservationSecurityContextChangedListenerTests.java +++ b/core/src/test/java/org/springframework/security/core/context/ObservationSecurityContextChangedListenerTests.java @@ -101,4 +101,23 @@ public class ObservationSecurityContextChangedListenerTests { .isEqualTo(ObservationSecurityContextChangedListener.SECURITY_CONTEXT_CREATED); } + @Test + void securityContextChangedWhenClearedEventThenAddsClearedEventToObservation() { + Observation observation = mock(Observation.class); + given(this.observationRegistry.getCurrentObservation()).willReturn(observation); + this.tested.securityContextChanged(new SecurityContextChangedEvent(this.one, new SecurityContextImpl())); + ArgumentCaptor event = ArgumentCaptor.forClass(Observation.Event.class); + verify(observation).event(event.capture()); + assertThat(event.getValue().getName()) + .isEqualTo(ObservationSecurityContextChangedListener.SECURITY_CONTEXT_CLEARED); + } + + @Test + void securityContextChangedWhenBothNullThenNoNullPointerException() { + Observation observation = mock(Observation.class); + given(this.observationRegistry.getCurrentObservation()).willReturn(observation); + this.tested.securityContextChanged( + new SecurityContextChangedEvent(new SecurityContextImpl(), new SecurityContextImpl())); + } + } diff --git a/core/src/test/java/org/springframework/security/core/userdetails/UserTests.java b/core/src/test/java/org/springframework/security/core/userdetails/UserTests.java index aa2a2387f6..1eeebae11e 100644 --- a/core/src/test/java/org/springframework/security/core/userdetails/UserTests.java +++ b/core/src/test/java/org/springframework/security/core/userdetails/UserTests.java @@ -141,12 +141,33 @@ public class UserTests { @Test public void testNullValuesRejected() { assertThatIllegalArgumentException().isThrownBy(() -> new User(null, "koala", true, true, true, true, ROLE_12)); - assertThatIllegalArgumentException().isThrownBy(() -> new User("rod", null, true, true, true, true, ROLE_12)); List auths = AuthorityUtils.createAuthorityList("ROLE_ONE"); auths.add(null); assertThatIllegalArgumentException().isThrownBy(() -> new User("rod", "koala", true, true, true, true, auths)); } + /** + * This is allowed because the password can become null when + * {@link User#eraseCredentials()} is called. + */ + @Test + public void constructorStringStringBooleanBooleanBooleanBooleanListWhenNullPasswordThenNullPassword() { + List auths = AuthorityUtils.createAuthorityList("ROLE_ONE"); + User rod = new User("rod", null, true, true, true, true, auths); + assertThat(rod.getPassword()).isNull(); + } + + /** + * This is allowed because the password can become null when + * {@link User#eraseCredentials()} is called. + */ + @Test + public void constructorStringStringListWhenNullPasswordThenNoException() { + List auths = AuthorityUtils.createAuthorityList("ROLE_ONE"); + User rod = new User("rod", null, auths); + assertThat(rod.getPassword()).isNull(); + } + @Test public void testNullWithinGrantedAuthorityElementIsRejected() { List auths = AuthorityUtils.createAuthorityList("ROLE_ONE"); @@ -254,4 +275,25 @@ public class UserTests { assertThat(withEncodedPassword.getPassword()).isEqualTo("passwordencoded"); } + /** + * This is allowed because the password can become null when + * {@link User#eraseCredentials()} is called. + */ + @Test + public void withUsernameWhenNullPasswordThenNoException() { + assertThat(User.withUsername("user").build().getPassword()).isNull(); + } + + @Test + public void withUsernameWhenPasswordNullAndEncoderThenEncoderNotUsed() { + Function encoder = (p) -> "encoded"; + // @formatter:off + UserDetails withEncodedPassword = User.withUsername("user") + .passwordEncoder(encoder) + .roles("USER") + .build(); + // @formatter:on + assertThat(withEncodedPassword.getPassword()).isNull(); + } + } diff --git a/core/src/test/java/org/springframework/security/provisioning/InMemoryUserDetailsManagerTests.java b/core/src/test/java/org/springframework/security/provisioning/InMemoryUserDetailsManagerTests.java index 9c97f1a51a..975ebe8182 100644 --- a/core/src/test/java/org/springframework/security/provisioning/InMemoryUserDetailsManagerTests.java +++ b/core/src/test/java/org/springframework/security/provisioning/InMemoryUserDetailsManagerTests.java @@ -42,6 +42,7 @@ import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.security.crypto.factory.PasswordEncoderFactories; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatException; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; import static org.mockito.BDDMockito.given; @@ -74,6 +75,17 @@ public class InMemoryUserDetailsManagerTests { .isEqualTo(newPassword); } + @Test + public void changePasswordWhenUserNotFoundThenAppropriateException() { + String newPassword = "newPassword"; + this.manager = new InMemoryUserDetailsManager(); + assertThatException().isThrownBy(() -> this.manager.updatePassword(this.user, newPassword)) + .isNotInstanceOf(NullPointerException.class) + // this is not failure to authenticate and UsernamePasswordNotFoundException + // extends AuthenticationException + .isNotInstanceOf(UsernameNotFoundException.class); + } + @Test public void constructorWhenUserPropertiesThenCreate() { Properties properties = new Properties(); diff --git a/core/src/test/java/org/springframework/security/provisioning/JdbcUserDetailsManagerTests.java b/core/src/test/java/org/springframework/security/provisioning/JdbcUserDetailsManagerTests.java index adb03ee9e8..27657da4c8 100644 --- a/core/src/test/java/org/springframework/security/provisioning/JdbcUserDetailsManagerTests.java +++ b/core/src/test/java/org/springframework/security/provisioning/JdbcUserDetailsManagerTests.java @@ -48,6 +48,7 @@ import org.springframework.security.core.userdetails.UserCache; import org.springframework.security.core.userdetails.UserDetails; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatException; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyInt; @@ -303,6 +304,14 @@ public class JdbcUserDetailsManagerTests { assertThat(this.template.queryForList("select id from groups")).isEmpty(); } + @Test + public void deleteGroupWhenNotFound() { + int groupCount = this.template.queryForList("select id from groups").size(); + assertThatException().isThrownBy(() -> this.manager.deleteGroup("MISSING")) + .isNotInstanceOf(NullPointerException.class); + assertThat(this.template.queryForList("select * from group_members")).hasSize(groupCount); + } + @Test public void renameGroupIsSuccessful() { this.manager.renameGroup("GROUP_0", "GROUP_X"); diff --git a/core/src/test/java/org/springframework/security/util/InMemoryResourceTests.java b/core/src/test/java/org/springframework/security/util/InMemoryResourceTests.java index 8d3f865e58..5a309ac220 100644 --- a/core/src/test/java/org/springframework/security/util/InMemoryResourceTests.java +++ b/core/src/test/java/org/springframework/security/util/InMemoryResourceTests.java @@ -28,7 +28,7 @@ public class InMemoryResourceTests { @Test public void resourceContainsExpectedData() throws Exception { InMemoryResource resource = new InMemoryResource("blah"); - assertThat(resource.getDescription()).isNull(); + assertThat(resource.getDescription()).isEmpty(); assertThat(resource.hashCode()).isEqualTo(1); assertThat(resource.getInputStream()).isNotNull(); } diff --git a/crypto/src/main/java/org/springframework/security/crypto/argon2/Argon2PasswordEncoder.java b/crypto/src/main/java/org/springframework/security/crypto/argon2/Argon2PasswordEncoder.java index 78759f8539..2b115aeee0 100644 --- a/crypto/src/main/java/org/springframework/security/crypto/argon2/Argon2PasswordEncoder.java +++ b/crypto/src/main/java/org/springframework/security/crypto/argon2/Argon2PasswordEncoder.java @@ -23,7 +23,7 @@ import org.bouncycastle.crypto.params.Argon2Parameters; import org.springframework.security.crypto.keygen.BytesKeyGenerator; import org.springframework.security.crypto.keygen.KeyGenerators; -import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.crypto.password.AbstractValidatingPasswordEncoder; /** *

@@ -44,7 +44,7 @@ import org.springframework.security.crypto.password.PasswordEncoder; * @author Simeon Macke * @since 5.3 */ -public class Argon2PasswordEncoder implements PasswordEncoder { +public class Argon2PasswordEncoder extends AbstractValidatingPasswordEncoder { private static final int DEFAULT_SALT_LENGTH = 16; @@ -108,7 +108,7 @@ public class Argon2PasswordEncoder implements PasswordEncoder { } @Override - public String encode(CharSequence rawPassword) { + protected String encodeNonNullPassword(String rawPassword) { byte[] salt = this.saltGenerator.generateKey(); byte[] hash = new byte[this.hashLength]; // @formatter:off @@ -127,11 +127,7 @@ public class Argon2PasswordEncoder implements PasswordEncoder { } @Override - public boolean matches(CharSequence rawPassword, String encodedPassword) { - if (encodedPassword == null) { - this.logger.warn("password hash is null"); - return false; - } + protected boolean matchesNonNull(String rawPassword, String encodedPassword) { Argon2EncodingUtils.Argon2Hash decoded; try { decoded = Argon2EncodingUtils.decode(encodedPassword); @@ -148,11 +144,7 @@ public class Argon2PasswordEncoder implements PasswordEncoder { } @Override - public boolean upgradeEncoding(String encodedPassword) { - if (encodedPassword == null || encodedPassword.length() == 0) { - this.logger.warn("password hash is null"); - return false; - } + protected boolean upgradeEncodingNonNull(String encodedPassword) { Argon2Parameters parameters = Argon2EncodingUtils.decode(encodedPassword).getParameters(); return parameters.getMemory() < this.memory || parameters.getIterations() < this.iterations; } diff --git a/crypto/src/main/java/org/springframework/security/crypto/bcrypt/BCryptPasswordEncoder.java b/crypto/src/main/java/org/springframework/security/crypto/bcrypt/BCryptPasswordEncoder.java index 84dfb08cd3..8bdb985c17 100644 --- a/crypto/src/main/java/org/springframework/security/crypto/bcrypt/BCryptPasswordEncoder.java +++ b/crypto/src/main/java/org/springframework/security/crypto/bcrypt/BCryptPasswordEncoder.java @@ -24,7 +24,7 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.jspecify.annotations.Nullable; -import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.crypto.password.AbstractValidatingPasswordEncoder; /** * Implementation of PasswordEncoder that uses the BCrypt strong hashing function. Clients @@ -34,7 +34,7 @@ import org.springframework.security.crypto.password.PasswordEncoder; * * @author Dave Syer */ -public class BCryptPasswordEncoder implements PasswordEncoder { +public class BCryptPasswordEncoder extends AbstractValidatingPasswordEncoder { private Pattern BCRYPT_PATTERN = Pattern.compile("\\A\\$2(a|y|b)?\\$(\\d\\d)\\$[./0-9A-Za-z]{53}"); @@ -103,10 +103,7 @@ public class BCryptPasswordEncoder implements PasswordEncoder { } @Override - public String encode(CharSequence rawPassword) { - if (rawPassword == null) { - throw new IllegalArgumentException("rawPassword cannot be null"); - } + protected String encodeNonNullPassword(String rawPassword) { String salt = getSalt(); return BCrypt.hashpw(rawPassword.toString(), salt); } @@ -119,14 +116,7 @@ public class BCryptPasswordEncoder implements PasswordEncoder { } @Override - public boolean matches(CharSequence rawPassword, String encodedPassword) { - if (rawPassword == null) { - throw new IllegalArgumentException("rawPassword cannot be null"); - } - if (encodedPassword == null || encodedPassword.length() == 0) { - this.logger.warn("Empty encoded password"); - return false; - } + protected boolean matchesNonNull(String rawPassword, String encodedPassword) { if (!this.BCRYPT_PATTERN.matcher(encodedPassword).matches()) { this.logger.warn("Encoded password does not look like BCrypt"); return false; @@ -135,11 +125,7 @@ public class BCryptPasswordEncoder implements PasswordEncoder { } @Override - public boolean upgradeEncoding(String encodedPassword) { - if (encodedPassword == null || encodedPassword.length() == 0) { - this.logger.warn("Empty encoded password"); - return false; - } + protected boolean upgradeEncodingNonNull(String encodedPassword) { Matcher matcher = this.BCRYPT_PATTERN.matcher(encodedPassword); if (!matcher.matches()) { throw new IllegalArgumentException("Encoded password does not look like BCrypt: " + encodedPassword); diff --git a/crypto/src/main/java/org/springframework/security/crypto/codec/Utf8.java b/crypto/src/main/java/org/springframework/security/crypto/codec/Utf8.java index a5d2e2f7e1..1b75dbecd7 100644 --- a/crypto/src/main/java/org/springframework/security/crypto/codec/Utf8.java +++ b/crypto/src/main/java/org/springframework/security/crypto/codec/Utf8.java @@ -22,8 +22,6 @@ import java.nio.charset.CharacterCodingException; import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; -import org.jspecify.annotations.Nullable; - /** * UTF-8 Charset encoder/decoder. *

@@ -41,7 +39,10 @@ public final class Utf8 { /** * Get the bytes of the String in UTF-8 encoded form. */ - public static byte[] encode(@Nullable CharSequence string) { + public static byte[] encode(CharSequence string) { + if (string == null) { + throw new IllegalArgumentException("String cannot be null"); + } try { ByteBuffer bytes = CHARSET.newEncoder().encode(CharBuffer.wrap(string)); byte[] bytesCopy = new byte[bytes.limit()]; diff --git a/crypto/src/main/java/org/springframework/security/crypto/password/AbstractPasswordEncoder.java b/crypto/src/main/java/org/springframework/security/crypto/password/AbstractPasswordEncoder.java index 180fb1863b..77662a76a7 100644 --- a/crypto/src/main/java/org/springframework/security/crypto/password/AbstractPasswordEncoder.java +++ b/crypto/src/main/java/org/springframework/security/crypto/password/AbstractPasswordEncoder.java @@ -28,7 +28,7 @@ import org.springframework.security.crypto.util.EncodingUtils; * * @author Rob Worsnop */ -public abstract class AbstractPasswordEncoder implements PasswordEncoder { +public abstract class AbstractPasswordEncoder extends AbstractValidatingPasswordEncoder { private final BytesKeyGenerator saltGenerator; @@ -37,29 +37,29 @@ public abstract class AbstractPasswordEncoder implements PasswordEncoder { } @Override - public String encode(CharSequence rawPassword) { + protected String encodeNonNullPassword(String rawPassword) { byte[] salt = this.saltGenerator.generateKey(); byte[] encoded = encodeAndConcatenate(rawPassword, salt); return String.valueOf(Hex.encode(encoded)); } @Override - public boolean matches(CharSequence rawPassword, String encodedPassword) { + protected boolean matchesNonNull(String rawPassword, String encodedPassword) { byte[] digested = Hex.decode(encodedPassword); byte[] salt = EncodingUtils.subArray(digested, 0, this.saltGenerator.getKeyLength()); - return matches(digested, encodeAndConcatenate(rawPassword, salt)); + return matchesNonNull(digested, encodeAndConcatenate(rawPassword, salt)); } - protected abstract byte[] encode(CharSequence rawPassword, byte[] salt); + protected abstract byte[] encodedNonNullPassword(CharSequence rawPassword, byte[] salt); protected byte[] encodeAndConcatenate(CharSequence rawPassword, byte[] salt) { - return EncodingUtils.concatenate(salt, encode(rawPassword, salt)); + return EncodingUtils.concatenate(salt, encodedNonNullPassword(rawPassword, salt)); } /** * Constant time comparison to prevent against timing attacks. */ - protected static boolean matches(byte[] expected, byte[] actual) { + protected static boolean matchesNonNull(byte[] expected, byte[] actual) { return MessageDigest.isEqual(expected, actual); } diff --git a/crypto/src/main/java/org/springframework/security/crypto/password/AbstractValidatingPasswordEncoder.java b/crypto/src/main/java/org/springframework/security/crypto/password/AbstractValidatingPasswordEncoder.java new file mode 100644 index 0000000000..a4eb298e4b --- /dev/null +++ b/crypto/src/main/java/org/springframework/security/crypto/password/AbstractValidatingPasswordEncoder.java @@ -0,0 +1,56 @@ +/* + * Copyright 2002-2025 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.crypto.password; + +import org.jspecify.annotations.Nullable; + +public abstract class AbstractValidatingPasswordEncoder implements PasswordEncoder { + + @Override + public final @Nullable String encode(@Nullable CharSequence rawPassword) { + if (rawPassword == null) { + return null; + } + return encodeNonNullPassword(rawPassword.toString()); + } + + protected abstract String encodeNonNullPassword(String rawPassword); + + @Override + public final boolean matches(@Nullable CharSequence rawPassword, @Nullable String encodedPassword) { + if (rawPassword == null || rawPassword.length() == 0 || encodedPassword == null + || encodedPassword.length() == 0) { + return false; + } + return matchesNonNull(rawPassword.toString(), encodedPassword); + } + + protected abstract boolean matchesNonNull(String rawPassword, String encodedPassword); + + @Override + public final boolean upgradeEncoding(@Nullable String encodedPassword) { + if (encodedPassword == null || encodedPassword.length() == 0) { + return false; + } + return upgradeEncodingNonNull(encodedPassword); + } + + protected boolean upgradeEncodingNonNull(String encodedPassword) { + return false; + } + +} diff --git a/crypto/src/main/java/org/springframework/security/crypto/password/DelegatingPasswordEncoder.java b/crypto/src/main/java/org/springframework/security/crypto/password/DelegatingPasswordEncoder.java index 5447aa557d..6e5ee6ddad 100644 --- a/crypto/src/main/java/org/springframework/security/crypto/password/DelegatingPasswordEncoder.java +++ b/crypto/src/main/java/org/springframework/security/crypto/password/DelegatingPasswordEncoder.java @@ -125,7 +125,7 @@ import org.jspecify.annotations.Nullable; * @since 5.0 * @see org.springframework.security.crypto.factory.PasswordEncoderFactories */ -public class DelegatingPasswordEncoder implements PasswordEncoder { +public class DelegatingPasswordEncoder extends AbstractValidatingPasswordEncoder { private static final String DEFAULT_ID_PREFIX = "{"; @@ -233,18 +233,12 @@ public class DelegatingPasswordEncoder implements PasswordEncoder { } @Override - public String encode(CharSequence rawPassword) { - if (rawPassword == null) { - throw new IllegalArgumentException("rawPassword cannot be null"); - } + protected String encodeNonNullPassword(String rawPassword) { return this.idPrefix + this.idForEncode + this.idSuffix + this.passwordEncoderForEncode.encode(rawPassword); } @Override - public boolean matches(CharSequence rawPassword, String prefixEncodedPassword) { - if (rawPassword == null && prefixEncodedPassword == null) { - return true; - } + protected boolean matchesNonNull(String rawPassword, String prefixEncodedPassword) { String id = extractId(prefixEncodedPassword); PasswordEncoder delegate = this.idToPasswordEncoder.get(id); if (delegate == null) { @@ -270,10 +264,7 @@ public class DelegatingPasswordEncoder implements PasswordEncoder { } @Override - public boolean upgradeEncoding(@Nullable String prefixEncodedPassword) { - if (prefixEncodedPassword == null) { - return false; - } + protected boolean upgradeEncodingNonNull(String prefixEncodedPassword) { String id = extractId(prefixEncodedPassword); if (!this.idForEncode.equalsIgnoreCase(id)) { return true; @@ -293,15 +284,15 @@ public class DelegatingPasswordEncoder implements PasswordEncoder { * Default {@link PasswordEncoder} that throws an exception telling that a suitable * {@link PasswordEncoder} for the id could not be found. */ - private class UnmappedIdPasswordEncoder implements PasswordEncoder { + private class UnmappedIdPasswordEncoder extends AbstractValidatingPasswordEncoder { @Override - public String encode(CharSequence rawPassword) { + protected String encodeNonNullPassword(String rawPassword) { throw new UnsupportedOperationException("encode is not supported"); } @Override - public boolean matches(CharSequence rawPassword, String prefixEncodedPassword) { + protected boolean matchesNonNull(String rawPassword, String prefixEncodedPassword) { String id = extractId(prefixEncodedPassword); if (id != null && !id.isBlank()) { throw new IllegalArgumentException(String.format(NO_PASSWORD_ENCODER_MAPPED, id)); diff --git a/crypto/src/main/java/org/springframework/security/crypto/password/LdapShaPasswordEncoder.java b/crypto/src/main/java/org/springframework/security/crypto/password/LdapShaPasswordEncoder.java index 40d12ce743..465f120768 100644 --- a/crypto/src/main/java/org/springframework/security/crypto/password/LdapShaPasswordEncoder.java +++ b/crypto/src/main/java/org/springframework/security/crypto/password/LdapShaPasswordEncoder.java @@ -46,7 +46,7 @@ import org.springframework.security.crypto.keygen.KeyGenerators; * indicate that this is a legacy implementation and using it is considered insecure. */ @Deprecated -public class LdapShaPasswordEncoder implements PasswordEncoder { +public class LdapShaPasswordEncoder extends AbstractValidatingPasswordEncoder { /** The number of bytes in a SHA hash */ private static final int SHA_LENGTH = 20; @@ -88,17 +88,17 @@ public class LdapShaPasswordEncoder implements PasswordEncoder { * Calculates the hash of password (and salt bytes, if supplied) and returns a base64 * encoded concatenation of the hash and salt, prefixed with {SHA} (or {SSHA} if salt * was used). - * @param rawPass the password to be encoded. + * @param rawPassword the password to be encoded. * @return the encoded password in the specified format * */ @Override - public String encode(CharSequence rawPass) { + protected String encodeNonNullPassword(String rawPassword) { byte[] salt = this.saltGenerator.generateKey(); - return encode(rawPass, salt); + return encode(rawPassword, salt); } - private String encode(@Nullable CharSequence rawPassword, byte @Nullable [] salt) { + private String encode(CharSequence rawPassword, byte @Nullable [] salt) { MessageDigest sha = getSha(rawPassword); if (salt != null) { sha.update(salt); @@ -108,7 +108,7 @@ public class LdapShaPasswordEncoder implements PasswordEncoder { return prefix + Utf8.decode(Base64.getEncoder().encode(hash)); } - private MessageDigest getSha(@Nullable CharSequence rawPassword) { + private MessageDigest getSha(CharSequence rawPassword) { try { MessageDigest sha = MessageDigest.getInstance("SHA"); sha.update(Utf8.encode(rawPassword)); @@ -143,11 +143,7 @@ public class LdapShaPasswordEncoder implements PasswordEncoder { * @return true if they match (independent of the case of the prefix). */ @Override - public boolean matches(CharSequence rawPassword, String encodedPassword) { - return matches((rawPassword != null) ? rawPassword.toString() : null, encodedPassword); - } - - private boolean matches(@Nullable String rawPassword, String encodedPassword) { + protected boolean matchesNonNull(String rawPassword, String encodedPassword) { String prefix = extractPrefix(encodedPassword); if (prefix == null) { return PasswordEncoderUtils.equals(encodedPassword, rawPassword); diff --git a/crypto/src/main/java/org/springframework/security/crypto/password/Md4PasswordEncoder.java b/crypto/src/main/java/org/springframework/security/crypto/password/Md4PasswordEncoder.java index f02583ae7f..fb47fec715 100644 --- a/crypto/src/main/java/org/springframework/security/crypto/password/Md4PasswordEncoder.java +++ b/crypto/src/main/java/org/springframework/security/crypto/password/Md4PasswordEncoder.java @@ -78,7 +78,7 @@ import org.springframework.security.crypto.keygen.StringKeyGenerator; * indicate that this is a legacy implementation and using it is considered insecure. */ @Deprecated -public class Md4PasswordEncoder implements PasswordEncoder { +public class Md4PasswordEncoder extends AbstractValidatingPasswordEncoder { private static final String PREFIX = "{"; @@ -100,7 +100,7 @@ public class Md4PasswordEncoder implements PasswordEncoder { * encodeHashAsBase64 is enabled. */ @Override - public String encode(CharSequence rawPassword) { + public String encodeNonNullPassword(String rawPassword) { String salt = PREFIX + this.saltGenerator.generateKey() + SUFFIX; return digest(salt, rawPassword); } @@ -114,11 +114,11 @@ public class Md4PasswordEncoder implements PasswordEncoder { Md4 md4 = new Md4(); md4.update(saltedPasswordBytes, 0, saltedPasswordBytes.length); byte[] digest = md4.digest(); - String encoded = encode(digest); + String encoded = encodedNonNullPassword(digest); return salt + encoded; } - private String encode(byte[] digest) { + private String encodedNonNullPassword(byte[] digest) { if (this.encodeHashAsBase64) { return Utf8.decode(Base64.getEncoder().encode(digest)); } @@ -133,7 +133,7 @@ public class Md4PasswordEncoder implements PasswordEncoder { * @return true or false */ @Override - public boolean matches(CharSequence rawPassword, String encodedPassword) { + protected boolean matchesNonNull(String rawPassword, String encodedPassword) { String salt = extractSalt(encodedPassword); String rawPasswordEncoded = digest(salt, rawPassword); return PasswordEncoderUtils.equals(encodedPassword.toString(), rawPasswordEncoded); diff --git a/crypto/src/main/java/org/springframework/security/crypto/password/MessageDigestPasswordEncoder.java b/crypto/src/main/java/org/springframework/security/crypto/password/MessageDigestPasswordEncoder.java index cdc6032677..027a500e37 100644 --- a/crypto/src/main/java/org/springframework/security/crypto/password/MessageDigestPasswordEncoder.java +++ b/crypto/src/main/java/org/springframework/security/crypto/password/MessageDigestPasswordEncoder.java @@ -82,7 +82,7 @@ import org.springframework.security.crypto.keygen.StringKeyGenerator; * indicate that this is a legacy implementation and using it is considered insecure. */ @Deprecated -public class MessageDigestPasswordEncoder implements PasswordEncoder { +public class MessageDigestPasswordEncoder extends AbstractValidatingPasswordEncoder { private static final String PREFIX = "{"; @@ -116,7 +116,7 @@ public class MessageDigestPasswordEncoder implements PasswordEncoder { * encodeHashAsBase64 is enabled. */ @Override - public String encode(CharSequence rawPassword) { + protected String encodeNonNullPassword(String rawPassword) { String salt = PREFIX + this.saltGenerator.generateKey() + SUFFIX; return digest(salt, rawPassword); } @@ -124,11 +124,11 @@ public class MessageDigestPasswordEncoder implements PasswordEncoder { private String digest(String salt, CharSequence rawPassword) { String saltedPassword = rawPassword + salt; byte[] digest = this.digester.digest(Utf8.encode(saltedPassword)); - String encoded = encode(digest); + String encoded = encodedNonNullPassword(digest); return salt + encoded; } - private String encode(byte[] digest) { + private String encodedNonNullPassword(byte[] digest) { if (this.encodeHashAsBase64) { return Utf8.decode(Base64.getEncoder().encode(digest)); } @@ -143,7 +143,7 @@ public class MessageDigestPasswordEncoder implements PasswordEncoder { * @return true or false */ @Override - public boolean matches(CharSequence rawPassword, String encodedPassword) { + protected boolean matchesNonNull(String rawPassword, String encodedPassword) { String salt = extractSalt(encodedPassword); String rawPasswordEncoded = digest(salt, rawPassword); return PasswordEncoderUtils.equals(encodedPassword.toString(), rawPasswordEncoded); diff --git a/crypto/src/main/java/org/springframework/security/crypto/password/NoOpPasswordEncoder.java b/crypto/src/main/java/org/springframework/security/crypto/password/NoOpPasswordEncoder.java index d092acf3ee..ae7ff57498 100644 --- a/crypto/src/main/java/org/springframework/security/crypto/password/NoOpPasswordEncoder.java +++ b/crypto/src/main/java/org/springframework/security/crypto/password/NoOpPasswordEncoder.java @@ -31,7 +31,7 @@ package org.springframework.security.crypto.password; * legacy implementation and using it is considered insecure. */ @Deprecated -public final class NoOpPasswordEncoder implements PasswordEncoder { +public final class NoOpPasswordEncoder extends AbstractValidatingPasswordEncoder { private static final PasswordEncoder INSTANCE = new NoOpPasswordEncoder(); @@ -39,12 +39,12 @@ public final class NoOpPasswordEncoder implements PasswordEncoder { } @Override - public String encode(CharSequence rawPassword) { + protected String encodeNonNullPassword(String rawPassword) { return rawPassword.toString(); } @Override - public boolean matches(CharSequence rawPassword, String encodedPassword) { + protected boolean matchesNonNull(String rawPassword, String encodedPassword) { return rawPassword.toString().equals(encodedPassword); } diff --git a/crypto/src/main/java/org/springframework/security/crypto/password/PasswordEncoder.java b/crypto/src/main/java/org/springframework/security/crypto/password/PasswordEncoder.java index 3d3f8ee425..862eb29d48 100644 --- a/crypto/src/main/java/org/springframework/security/crypto/password/PasswordEncoder.java +++ b/crypto/src/main/java/org/springframework/security/crypto/password/PasswordEncoder.java @@ -16,40 +16,50 @@ package org.springframework.security.crypto.password; +import org.jspecify.annotations.Nullable; + /** * Service interface for encoding passwords. * * The preferred implementation is {@code BCryptPasswordEncoder}. * * @author Keith Donald + * @author Rob Winch */ public interface PasswordEncoder { /** - * Encode the raw password. Generally, a good encoding algorithm applies a SHA-1 or - * greater hash combined with an 8-byte or greater randomly generated salt. + * Encode the raw password. Generally, a good encoding algorithm uses an adaptive one + * way function. + * @param rawPassword a password that has not been encoded. The value can be null in + * the event that the user has no password; in which case the result must be null. + * @return A non-null encoded password, unless the rawPassword was null in which case + * the result must be null. */ - String encode(CharSequence rawPassword); + @Nullable String encode(@Nullable CharSequence rawPassword); /** * Verify the encoded password obtained from storage matches the submitted raw * password after it too is encoded. Returns true if the passwords match, false if - * they do not. The stored password itself is never decoded. - * @param rawPassword the raw password to encode and match - * @param encodedPassword the encoded password from storage to compare with + * they do not. The stored password itself is never decoded. Never true if either + * rawPassword or encodedPassword is null or an empty String. + * @param rawPassword the raw password to encode and match. + * @param encodedPassword the encoded password from storage to compare with. * @return true if the raw password, after encoding, matches the encoded password from - * storage + * storage. */ - boolean matches(CharSequence rawPassword, String encodedPassword); + boolean matches(@Nullable CharSequence rawPassword, @Nullable String encodedPassword); /** * Returns true if the encoded password should be encoded again for better security, * else false. The default implementation always returns false. - * @param encodedPassword the encoded password to check + * @param encodedPassword the encoded password to check. Possibly null if the user did + * not have a password. * @return true if the encoded password should be encoded again for better security, - * else false. + * else false. If encodedPassword is null (the user didn't have a password), then + * always false. */ - default boolean upgradeEncoding(String encodedPassword) { + default boolean upgradeEncoding(@Nullable String encodedPassword) { return false; } diff --git a/crypto/src/main/java/org/springframework/security/crypto/password/Pbkdf2PasswordEncoder.java b/crypto/src/main/java/org/springframework/security/crypto/password/Pbkdf2PasswordEncoder.java index cecebea0bf..252a2aba75 100644 --- a/crypto/src/main/java/org/springframework/security/crypto/password/Pbkdf2PasswordEncoder.java +++ b/crypto/src/main/java/org/springframework/security/crypto/password/Pbkdf2PasswordEncoder.java @@ -46,7 +46,7 @@ import org.springframework.security.crypto.util.EncodingUtils; * @author Loïc Guibert * @since 4.1 */ -public class Pbkdf2PasswordEncoder implements PasswordEncoder { +public class Pbkdf2PasswordEncoder extends AbstractValidatingPasswordEncoder { private static final int DEFAULT_SALT_LENGTH = 16; @@ -194,13 +194,13 @@ public class Pbkdf2PasswordEncoder implements PasswordEncoder { } @Override - public String encode(CharSequence rawPassword) { + protected String encodeNonNullPassword(String rawPassword) { byte[] salt = this.saltGenerator.generateKey(); - byte[] encoded = encode(rawPassword, salt); - return encode(encoded); + byte[] encoded = encodedNonNullPassword(rawPassword, salt); + return encodedNonNullPassword(encoded); } - private String encode(byte[] bytes) { + private String encodedNonNullPassword(byte[] bytes) { if (this.encodeHashAsBase64) { return Base64.getEncoder().encodeToString(bytes); } @@ -208,10 +208,10 @@ public class Pbkdf2PasswordEncoder implements PasswordEncoder { } @Override - public boolean matches(CharSequence rawPassword, String encodedPassword) { + protected boolean matchesNonNull(String rawPassword, String encodedPassword) { byte[] digested = decode(encodedPassword); byte[] salt = EncodingUtils.subArray(digested, 0, this.saltGenerator.getKeyLength()); - return MessageDigest.isEqual(digested, encode(rawPassword, salt)); + return MessageDigest.isEqual(digested, encodedNonNullPassword(rawPassword, salt)); } private byte[] decode(String encodedBytes) { @@ -221,7 +221,7 @@ public class Pbkdf2PasswordEncoder implements PasswordEncoder { return Hex.decode(encodedBytes); } - private byte[] encode(CharSequence rawPassword, byte[] salt) { + private byte[] encodedNonNullPassword(CharSequence rawPassword, byte[] salt) { try { PBEKeySpec spec = new PBEKeySpec(rawPassword.toString().toCharArray(), EncodingUtils.concatenate(salt, this.secret), this.iterations, this.hashWidth); diff --git a/crypto/src/main/java/org/springframework/security/crypto/password/StandardPasswordEncoder.java b/crypto/src/main/java/org/springframework/security/crypto/password/StandardPasswordEncoder.java index e19e445ce2..62cb18a7d1 100644 --- a/crypto/src/main/java/org/springframework/security/crypto/password/StandardPasswordEncoder.java +++ b/crypto/src/main/java/org/springframework/security/crypto/password/StandardPasswordEncoder.java @@ -48,7 +48,7 @@ import org.springframework.security.crypto.util.EncodingUtils; * indicate that this is a legacy implementation and using it is considered insecure. */ @Deprecated -public final class StandardPasswordEncoder implements PasswordEncoder { +public final class StandardPasswordEncoder extends AbstractValidatingPasswordEncoder { private static final int DEFAULT_ITERATIONS = 1024; @@ -75,12 +75,12 @@ public final class StandardPasswordEncoder implements PasswordEncoder { } @Override - public String encode(CharSequence rawPassword) { - return encode(rawPassword, this.saltGenerator.generateKey()); + protected String encodeNonNullPassword(String rawPassword) { + return encodedNonNullPassword(rawPassword, this.saltGenerator.generateKey()); } @Override - public boolean matches(CharSequence rawPassword, String encodedPassword) { + protected boolean matchesNonNull(String rawPassword, String encodedPassword) { byte[] digested = decode(encodedPassword); byte[] salt = EncodingUtils.subArray(digested, 0, this.saltGenerator.getKeyLength()); return MessageDigest.isEqual(digested, digest(rawPassword, salt)); @@ -92,7 +92,7 @@ public final class StandardPasswordEncoder implements PasswordEncoder { this.saltGenerator = KeyGenerators.secureRandom(); } - private String encode(CharSequence rawPassword, byte[] salt) { + private String encodedNonNullPassword(CharSequence rawPassword, byte[] salt) { byte[] digest = digest(rawPassword, salt); return new String(Hex.encode(digest)); } diff --git a/crypto/src/main/java/org/springframework/security/crypto/scrypt/SCryptPasswordEncoder.java b/crypto/src/main/java/org/springframework/security/crypto/scrypt/SCryptPasswordEncoder.java index 2922a8b9d5..41bcc36485 100644 --- a/crypto/src/main/java/org/springframework/security/crypto/scrypt/SCryptPasswordEncoder.java +++ b/crypto/src/main/java/org/springframework/security/crypto/scrypt/SCryptPasswordEncoder.java @@ -26,7 +26,7 @@ import org.bouncycastle.crypto.generators.SCrypt; import org.springframework.security.crypto.codec.Utf8; import org.springframework.security.crypto.keygen.BytesKeyGenerator; import org.springframework.security.crypto.keygen.KeyGenerators; -import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.crypto.password.AbstractValidatingPasswordEncoder; /** *

@@ -56,7 +56,7 @@ import org.springframework.security.crypto.password.PasswordEncoder; * @author Rob Winch * */ -public class SCryptPasswordEncoder implements PasswordEncoder { +public class SCryptPasswordEncoder extends AbstractValidatingPasswordEncoder { private static final int DEFAULT_CPU_COST = 65536; @@ -146,24 +146,17 @@ public class SCryptPasswordEncoder implements PasswordEncoder { } @Override - public String encode(CharSequence rawPassword) { + protected String encodeNonNullPassword(String rawPassword) { return digest(rawPassword, this.saltGenerator.generateKey()); } @Override - public boolean matches(CharSequence rawPassword, String encodedPassword) { - if (encodedPassword == null || encodedPassword.length() < this.keyLength) { - this.logger.warn("Empty encoded password"); - return false; - } + protected boolean matchesNonNull(String rawPassword, String encodedPassword) { return decodeAndCheckMatches(rawPassword, encodedPassword); } @Override - public boolean upgradeEncoding(String encodedPassword) { - if (encodedPassword == null || encodedPassword.isEmpty()) { - return false; - } + protected boolean upgradeEncodingNonNull(String encodedPassword) { String[] parts = encodedPassword.split("\\$"); if (parts.length != 4) { throw new IllegalArgumentException("Encoded password does not look like SCrypt: " + encodedPassword); diff --git a/crypto/src/test/java/org/springframework/security/crypto/argon2/Argon2PasswordEncoderTests.java b/crypto/src/test/java/org/springframework/security/crypto/argon2/Argon2PasswordEncoderTests.java index ea0053be3c..2d0dfad72b 100644 --- a/crypto/src/test/java/org/springframework/security/crypto/argon2/Argon2PasswordEncoderTests.java +++ b/crypto/src/test/java/org/springframework/security/crypto/argon2/Argon2PasswordEncoderTests.java @@ -19,6 +19,7 @@ package org.springframework.security.crypto.argon2; import java.lang.reflect.Field; import java.util.Arrays; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; @@ -26,6 +27,7 @@ import org.mockito.Mockito; import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.security.crypto.keygen.BytesKeyGenerator; +import org.springframework.security.crypto.password.AbstractPasswordEncoderValidationTests; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; @@ -34,56 +36,59 @@ import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException * @author Simeon Macke */ @ExtendWith(MockitoExtension.class) -public class Argon2PasswordEncoderTests { +public class Argon2PasswordEncoderTests extends AbstractPasswordEncoderValidationTests { @Mock private BytesKeyGenerator keyGeneratorMock; - private Argon2PasswordEncoder encoder = Argon2PasswordEncoder.defaultsForSpringSecurity_v5_2(); + @BeforeEach + void setup() { + setEncoder(Argon2PasswordEncoder.defaultsForSpringSecurity_v5_2()); + } @Test - public void encodeDoesNotEqualPassword() { - String result = this.encoder.encode("password"); + public void encodedNonNullPasswordDoesNotEqualPassword() { + String result = getEncoder().encode("password"); assertThat(result).isNotEqualTo("password"); } @Test - public void encodeWhenEqualPasswordThenMatches() { - String result = this.encoder.encode("password"); - assertThat(this.encoder.matches("password", result)).isTrue(); + public void encodedNonNullPasswordWhenEqualPasswordThenMatches() { + String result = getEncoder().encode("password"); + assertThat(getEncoder().matches("password", result)).isTrue(); } @Test - public void encodeWhenEqualWithUnicodeThenMatches() { - String result = this.encoder.encode("passw\u9292rd"); - assertThat(this.encoder.matches("pass\u9292\u9292rd", result)).isFalse(); - assertThat(this.encoder.matches("passw\u9292rd", result)).isTrue(); + public void encodedNonNullPasswordWhenEqualWithUnicodeThenMatches() { + String result = getEncoder().encode("passw\u9292rd"); + assertThat(getEncoder().matches("pass\u9292\u9292rd", result)).isFalse(); + assertThat(getEncoder().matches("passw\u9292rd", result)).isTrue(); } @Test - public void encodeWhenNotEqualThenNotMatches() { - String result = this.encoder.encode("password"); - assertThat(this.encoder.matches("bogus", result)).isFalse(); + public void encodedNonNullPasswordWhenNotEqualThenNotMatches() { + String result = getEncoder().encode("password"); + assertThat(getEncoder().matches("bogus", result)).isFalse(); } @Test - public void encodeWhenEqualPasswordWithCustomParamsThenMatches() { - this.encoder = new Argon2PasswordEncoder(20, 64, 4, 256, 4); - String result = this.encoder.encode("password"); - assertThat(this.encoder.matches("password", result)).isTrue(); + public void encodedNonNullPasswordWhenEqualPasswordWithCustomParamsThenMatches() { + setEncoder(new Argon2PasswordEncoder(20, 64, 4, 256, 4)); + String result = getEncoder().encode("password"); + assertThat(getEncoder().matches("password", result)).isTrue(); } @Test - public void encodeWhenRanTwiceThenResultsNotEqual() { + public void encodedNonNullPasswordWhenRanTwiceThenResultsNotEqual() { String password = "secret"; - assertThat(this.encoder.encode(password)).isNotEqualTo(this.encoder.encode(password)); + assertThat(getEncoder().encode(password)).isNotEqualTo(getEncoder().encode(password)); } @Test - public void encodeWhenRanTwiceWithCustomParamsThenNotEquals() { - this.encoder = new Argon2PasswordEncoder(20, 64, 4, 256, 4); + public void encodedNonNullPasswordWhenRanTwiceWithCustomParamsThenNotEquals() { + setEncoder(new Argon2PasswordEncoder(20, 64, 4, 256, 4)); String password = "secret"; - assertThat(this.encoder.encode(password)).isNotEqualTo(this.encoder.encode(password)); + assertThat(getEncoder().encode(password)).isNotEqualTo(getEncoder().encode(password)); } @Test @@ -97,55 +102,56 @@ public class Argon2PasswordEncoderTests { @Test public void matchesWhenEncodedPassIsNullThenFalse() { - assertThat(this.encoder.matches("password", null)).isFalse(); + assertThat(getEncoder().matches("password", null)).isFalse(); } @Test public void matchesWhenEncodedPassIsEmptyThenFalse() { - assertThat(this.encoder.matches("password", "")).isFalse(); + assertThat(getEncoder().matches("password", "")).isFalse(); } @Test public void matchesWhenEncodedPassIsBogusThenFalse() { - assertThat(this.encoder.matches("password", "012345678901234567890123456789")).isFalse(); + assertThat(getEncoder().matches("password", "012345678901234567890123456789")).isFalse(); } @Test - public void encodeWhenUsingPredictableSaltThenEqualTestHash() throws Exception { + public void encodedNonNullPasswordWhenUsingPredictableSaltThenEqualTestHash() throws Exception { injectPredictableSaltGen(); - String hash = this.encoder.encode("sometestpassword"); + String hash = getEncoder().encode("sometestpassword"); assertThat(hash).isEqualTo( "$argon2id$v=19$m=4096,t=3,p=1$QUFBQUFBQUFBQUFBQUFBQQ$hmmTNyJlwbb6HAvFoHFWF+u03fdb0F2qA+39oPlcAqo"); } @Test - public void encodeWhenUsingPredictableSaltWithCustomParamsThenEqualTestHash() throws Exception { - this.encoder = new Argon2PasswordEncoder(16, 32, 4, 512, 5); + public void encodedNonNullPasswordWhenUsingPredictableSaltWithCustomParamsThenEqualTestHash() throws Exception { + setEncoder(new Argon2PasswordEncoder(16, 32, 4, 512, 5)); injectPredictableSaltGen(); - String hash = this.encoder.encode("sometestpassword"); + String hash = getEncoder().encode("sometestpassword"); assertThat(hash).isEqualTo( "$argon2id$v=19$m=512,t=5,p=4$QUFBQUFBQUFBQUFBQUFBQQ$PNv4C3K50bz3rmON+LtFpdisD7ePieLNq+l5iUHgc1k"); } @Test - public void encodeWhenUsingPredictableSaltWithDefaultsForSpringSecurity_v5_8ThenEqualTestHash() throws Exception { - this.encoder = Argon2PasswordEncoder.defaultsForSpringSecurity_v5_8(); + public void encodedNonNullPasswordWhenUsingPredictableSaltWithDefaultsForSpringSecurity_v5_8ThenEqualTestHash() + throws Exception { + setEncoder(Argon2PasswordEncoder.defaultsForSpringSecurity_v5_8()); injectPredictableSaltGen(); - String hash = this.encoder.encode("sometestpassword"); + String hash = getEncoder().encode("sometestpassword"); assertThat(hash).isEqualTo( "$argon2id$v=19$m=16384,t=2,p=1$QUFBQUFBQUFBQUFBQUFBQQ$zGt5MiNPSUOo4/7jBcJMayCPfcsLJ4c0WUxhwGDIYPw"); } @Test public void upgradeEncodingWhenSameEncodingThenFalse() { - String hash = this.encoder.encode("password"); - assertThat(this.encoder.upgradeEncoding(hash)).isFalse(); + String hash = getEncoder().encode("password"); + assertThat(getEncoder().upgradeEncoding(hash)).isFalse(); } @Test public void upgradeEncodingWhenSameStandardParamsThenFalse() { Argon2PasswordEncoder newEncoder = Argon2PasswordEncoder.defaultsForSpringSecurity_v5_2(); - String hash = this.encoder.encode("password"); + String hash = getEncoder().encode("password"); assertThat(newEncoder.upgradeEncoding(hash)).isFalse(); } @@ -183,17 +189,17 @@ public class Argon2PasswordEncoderTests { @Test public void upgradeEncodingWhenEncodedPassIsNullThenFalse() { - assertThat(this.encoder.upgradeEncoding(null)).isFalse(); + assertThat(getEncoder().upgradeEncoding(null)).isFalse(); } @Test public void upgradeEncodingWhenEncodedPassIsEmptyThenFalse() { - assertThat(this.encoder.upgradeEncoding("")).isFalse(); + assertThat(getEncoder().upgradeEncoding("")).isFalse(); } @Test public void upgradeEncodingWhenEncodedPassIsBogusThenThrowException() { - assertThatIllegalArgumentException().isThrownBy(() -> this.encoder.upgradeEncoding("thisIsNoValidHash")); + assertThatIllegalArgumentException().isThrownBy(() -> getEncoder().upgradeEncoding("thisIsNoValidHash")); } private void injectPredictableSaltGen() throws Exception { @@ -203,9 +209,9 @@ public class Argon2PasswordEncoderTests { // we can't use the @InjectMock-annotation because the salt-generator is set in // the constructor // and Mockito will only inject mocks if they are null - Field saltGen = this.encoder.getClass().getDeclaredField("saltGenerator"); + Field saltGen = getEncoder().getClass().getDeclaredField("saltGenerator"); saltGen.setAccessible(true); - saltGen.set(this.encoder, this.keyGeneratorMock); + saltGen.set(getEncoder(), this.keyGeneratorMock); saltGen.setAccessible(false); } diff --git a/crypto/src/test/java/org/springframework/security/crypto/bcrypt/BCryptPasswordEncoderTests.java b/crypto/src/test/java/org/springframework/security/crypto/bcrypt/BCryptPasswordEncoderTests.java index f2921064fa..d8b9d51bad 100644 --- a/crypto/src/test/java/org/springframework/security/crypto/bcrypt/BCryptPasswordEncoderTests.java +++ b/crypto/src/test/java/org/springframework/security/crypto/bcrypt/BCryptPasswordEncoderTests.java @@ -18,8 +18,11 @@ package org.springframework.security.crypto.bcrypt; import java.security.SecureRandom; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.springframework.security.crypto.password.AbstractPasswordEncoderValidationTests; + import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; @@ -27,107 +30,107 @@ import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException * @author Dave Syer * */ -public class BCryptPasswordEncoderTests { +public class BCryptPasswordEncoderTests extends AbstractPasswordEncoderValidationTests { + + @BeforeEach + void setup() { + setEncoder(new BCryptPasswordEncoder()); + } @Test // gh-5548 public void emptyRawPasswordDoesNotMatchPassword() { - BCryptPasswordEncoder encoder = new BCryptPasswordEncoder(); - String result = encoder.encode("password"); - assertThat(encoder.matches("", result)).isFalse(); + String result = getEncoder().encode("password"); + assertThat(getEncoder().matches("", result)).isFalse(); } @Test public void $2yMatches() { // $2y is default version - BCryptPasswordEncoder encoder = new BCryptPasswordEncoder(); - String result = encoder.encode("password"); + String result = getEncoder().encode("password"); assertThat(result.equals("password")).isFalse(); - assertThat(encoder.matches("password", result)).isTrue(); + assertThat(getEncoder().matches("password", result)).isTrue(); } @Test public void $2aMatches() { - BCryptPasswordEncoder encoder = new BCryptPasswordEncoder(BCryptPasswordEncoder.BCryptVersion.$2A); - String result = encoder.encode("password"); + String result = getEncoder().encode("password"); assertThat(result.equals("password")).isFalse(); - assertThat(encoder.matches("password", result)).isTrue(); + assertThat(getEncoder().matches("password", result)).isTrue(); } @Test public void $2bMatches() { - BCryptPasswordEncoder encoder = new BCryptPasswordEncoder(BCryptPasswordEncoder.BCryptVersion.$2B); - String result = encoder.encode("password"); + setEncoder(new BCryptPasswordEncoder(BCryptPasswordEncoder.BCryptVersion.$2B)); + String result = getEncoder().encode("password"); assertThat(result.equals("password")).isFalse(); - assertThat(encoder.matches("password", result)).isTrue(); + assertThat(getEncoder().matches("password", result)).isTrue(); } @Test public void $2yUnicode() { // $2y is default version - BCryptPasswordEncoder encoder = new BCryptPasswordEncoder(); - String result = encoder.encode("passw\u9292rd"); - assertThat(encoder.matches("pass\u9292\u9292rd", result)).isFalse(); - assertThat(encoder.matches("passw\u9292rd", result)).isTrue(); + String result = getEncoder().encode("passw\u9292rd"); + assertThat(getEncoder().matches("pass\u9292\u9292rd", result)).isFalse(); + assertThat(getEncoder().matches("passw\u9292rd", result)).isTrue(); } @Test public void $2aUnicode() { - BCryptPasswordEncoder encoder = new BCryptPasswordEncoder(BCryptPasswordEncoder.BCryptVersion.$2A); - String result = encoder.encode("passw\u9292rd"); - assertThat(encoder.matches("pass\u9292\u9292rd", result)).isFalse(); - assertThat(encoder.matches("passw\u9292rd", result)).isTrue(); + setEncoder(new BCryptPasswordEncoder(BCryptPasswordEncoder.BCryptVersion.$2A)); + String result = getEncoder().encode("passw\u9292rd"); + assertThat(getEncoder().matches("pass\u9292\u9292rd", result)).isFalse(); + assertThat(getEncoder().matches("passw\u9292rd", result)).isTrue(); } @Test public void $2bUnicode() { - BCryptPasswordEncoder encoder = new BCryptPasswordEncoder(BCryptPasswordEncoder.BCryptVersion.$2B); - String result = encoder.encode("passw\u9292rd"); - assertThat(encoder.matches("pass\u9292\u9292rd", result)).isFalse(); - assertThat(encoder.matches("passw\u9292rd", result)).isTrue(); + setEncoder(new BCryptPasswordEncoder(BCryptPasswordEncoder.BCryptVersion.$2B)); + String result = getEncoder().encode("passw\u9292rd"); + assertThat(getEncoder().matches("pass\u9292\u9292rd", result)).isFalse(); + assertThat(getEncoder().matches("passw\u9292rd", result)).isTrue(); } @Test public void $2yNotMatches() { // $2y is default version - BCryptPasswordEncoder encoder = new BCryptPasswordEncoder(); - String result = encoder.encode("password"); - assertThat(encoder.matches("bogus", result)).isFalse(); + String result = getEncoder().encode("password"); + assertThat(getEncoder().matches("bogus", result)).isFalse(); } @Test public void $2aNotMatches() { - BCryptPasswordEncoder encoder = new BCryptPasswordEncoder(BCryptPasswordEncoder.BCryptVersion.$2A); - String result = encoder.encode("password"); - assertThat(encoder.matches("bogus", result)).isFalse(); + setEncoder(new BCryptPasswordEncoder(BCryptPasswordEncoder.BCryptVersion.$2A)); + String result = getEncoder().encode("password"); + assertThat(getEncoder().matches("bogus", result)).isFalse(); } @Test public void $2bNotMatches() { - BCryptPasswordEncoder encoder = new BCryptPasswordEncoder(BCryptPasswordEncoder.BCryptVersion.$2B); - String result = encoder.encode("password"); - assertThat(encoder.matches("bogus", result)).isFalse(); + setEncoder(new BCryptPasswordEncoder(BCryptPasswordEncoder.BCryptVersion.$2B)); + String result = getEncoder().encode("password"); + assertThat(getEncoder().matches("bogus", result)).isFalse(); } @Test public void $2yCustomStrength() { - BCryptPasswordEncoder encoder = new BCryptPasswordEncoder(8); - String result = encoder.encode("password"); - assertThat(encoder.matches("password", result)).isTrue(); + setEncoder(new BCryptPasswordEncoder(8)); + String result = getEncoder().encode("password"); + assertThat(getEncoder().matches("password", result)).isTrue(); } @Test public void $2aCustomStrength() { - BCryptPasswordEncoder encoder = new BCryptPasswordEncoder(BCryptPasswordEncoder.BCryptVersion.$2A, 8); - String result = encoder.encode("password"); - assertThat(encoder.matches("password", result)).isTrue(); + setEncoder(new BCryptPasswordEncoder(BCryptPasswordEncoder.BCryptVersion.$2A, 8)); + String result = getEncoder().encode("password"); + assertThat(getEncoder().matches("password", result)).isTrue(); } @Test public void $2bCustomStrength() { - BCryptPasswordEncoder encoder = new BCryptPasswordEncoder(BCryptPasswordEncoder.BCryptVersion.$2B, 8); - String result = encoder.encode("password"); - assertThat(encoder.matches("password", result)).isTrue(); + setEncoder(new BCryptPasswordEncoder(BCryptPasswordEncoder.BCryptVersion.$2B, 8)); + String result = getEncoder().encode("password"); + assertThat(getEncoder().matches("password", result)).isTrue(); } @Test @@ -142,27 +145,25 @@ public class BCryptPasswordEncoderTests { @Test public void customRandom() { - BCryptPasswordEncoder encoder = new BCryptPasswordEncoder(8, new SecureRandom()); - String result = encoder.encode("password"); - assertThat(encoder.matches("password", result)).isTrue(); + setEncoder(new BCryptPasswordEncoder(8, new SecureRandom())); + String result = getEncoder().encode("password"); + assertThat(getEncoder().matches("password", result)).isTrue(); } @Test public void doesntMatchNullEncodedValue() { - BCryptPasswordEncoder encoder = new BCryptPasswordEncoder(); - assertThat(encoder.matches("password", null)).isFalse(); + setEncoder(new BCryptPasswordEncoder()); + assertThat(getEncoder().matches("password", null)).isFalse(); } @Test public void doesntMatchEmptyEncodedValue() { - BCryptPasswordEncoder encoder = new BCryptPasswordEncoder(); - assertThat(encoder.matches("password", "")).isFalse(); + assertThat(getEncoder().matches("password", "")).isFalse(); } @Test public void doesntMatchBogusEncodedValue() { - BCryptPasswordEncoder encoder = new BCryptPasswordEncoder(); - assertThat(encoder.matches("password", "012345678901234567890123456789")).isFalse(); + assertThat(getEncoder().matches("password", "012345678901234567890123456789")).isFalse(); } @Test @@ -181,9 +182,8 @@ public class BCryptPasswordEncoderTests { */ @Test public void upgradeFromNullOrEmpty() { - BCryptPasswordEncoder encoder = new BCryptPasswordEncoder(); - assertThat(encoder.upgradeEncoding(null)).isFalse(); - assertThat(encoder.upgradeEncoding("")).isFalse(); + assertThat(getEncoder().upgradeEncoding(null)).isFalse(); + assertThat(getEncoder().upgradeEncoding("")).isFalse(); } /** @@ -192,65 +192,48 @@ public class BCryptPasswordEncoderTests { */ @Test public void upgradeFromNonBCrypt() { - BCryptPasswordEncoder encoder = new BCryptPasswordEncoder(); - assertThatIllegalArgumentException().isThrownBy(() -> encoder.upgradeEncoding("not-a-bcrypt-password")); - } - - @Test - public void encodeNullRawPassword() { - BCryptPasswordEncoder encoder = new BCryptPasswordEncoder(); - assertThatIllegalArgumentException().isThrownBy(() -> encoder.encode(null)); - } - - @Test - public void matchNullRawPassword() { - BCryptPasswordEncoder encoder = new BCryptPasswordEncoder(); - assertThatIllegalArgumentException().isThrownBy(() -> encoder.matches(null, "does-not-matter")); + assertThatIllegalArgumentException().isThrownBy(() -> getEncoder().upgradeEncoding("not-a-bcrypt-password")); } @Test public void upgradeWhenNoRoundsThenTrue() { - BCryptPasswordEncoder encoder = new BCryptPasswordEncoder(); - assertThat(encoder.upgradeEncoding("$2a$00$9N8N35BVs5TLqGL3pspAte5OWWA2a2aZIs.EGp7At7txYakFERMue")).isTrue(); + assertThat(getEncoder().upgradeEncoding("$2a$00$9N8N35BVs5TLqGL3pspAte5OWWA2a2aZIs.EGp7At7txYakFERMue")) + .isTrue(); } @Test public void checkWhenNoRoundsThenTrue() { - BCryptPasswordEncoder encoder = new BCryptPasswordEncoder(); - assertThat(encoder.matches("password", "$2a$00$9N8N35BVs5TLqGL3pspAte5OWWA2a2aZIs.EGp7At7txYakFERMue")) + assertThat(getEncoder().matches("password", "$2a$00$9N8N35BVs5TLqGL3pspAte5OWWA2a2aZIs.EGp7At7txYakFERMue")) .isTrue(); - assertThat(encoder.matches("wrong", "$2a$00$9N8N35BVs5TLqGL3pspAte5OWWA2a2aZIs.EGp7At7txYakFERMue")).isFalse(); + assertThat(getEncoder().matches("wrong", "$2a$00$9N8N35BVs5TLqGL3pspAte5OWWA2a2aZIs.EGp7At7txYakFERMue")) + .isFalse(); } @Test public void encodeWhenPasswordOverMaxLengthThenThrowIllegalArgumentException() { - BCryptPasswordEncoder encoder = new BCryptPasswordEncoder(); - String password72chars = "123456789012345678901234567890123456789012345678901234567890123456789012"; - encoder.encode(password72chars); + getEncoder().encode(password72chars); String password73chars = password72chars + "3"; - assertThatIllegalArgumentException().isThrownBy(() -> encoder.encode(password73chars)); + assertThatIllegalArgumentException().isThrownBy(() -> getEncoder().encode(password73chars)); } @Test public void matchesWhenPasswordOverMaxLengthThenAllowToMatch() { - BCryptPasswordEncoder encoder = new BCryptPasswordEncoder(); - String password71chars = "12345678901234567890123456789012345678901234567890123456789012345678901"; String encodedPassword71chars = "$2a$10$jx3x2FaF.iX5QZ9i3O424Os2Ou5P5JrnedmWYHuDyX8JKA4Unp4xq"; - assertThat(encoder.matches(password71chars, encodedPassword71chars)).isTrue(); + assertThat(getEncoder().matches(password71chars, encodedPassword71chars)).isTrue(); String password72chars = password71chars + "2"; String encodedPassword72chars = "$2a$10$oXYO6/UvbsH5rQEraBkl6uheccBqdB3n.RaWbrimog9hS2GX4lo/O"; - assertThat(encoder.matches(password72chars, encodedPassword72chars)).isTrue(); + assertThat(getEncoder().matches(password72chars, encodedPassword72chars)).isTrue(); // Max length is 72 bytes, however, we need to ensure backwards compatibility // for previously encoded passwords that are greater than 72 bytes and allow the // match to be performed. String password73chars = password72chars + "3"; String encodedPassword73chars = "$2a$10$1l9.kvQTsqNLiCYFqmKtQOHkp.BrgIrwsnTzWo9jdbQRbuBYQ/AVK"; - assertThat(encoder.matches(password73chars, encodedPassword73chars)).isTrue(); + assertThat(getEncoder().matches(password73chars, encodedPassword73chars)).isTrue(); } } diff --git a/crypto/src/test/java/org/springframework/security/crypto/password/AbstractPasswordEncoderTests.java b/crypto/src/test/java/org/springframework/security/crypto/password/AbstractPasswordEncoderTests.java new file mode 100644 index 0000000000..8730f8587d --- /dev/null +++ b/crypto/src/test/java/org/springframework/security/crypto/password/AbstractPasswordEncoderTests.java @@ -0,0 +1,40 @@ +/* + * Copyright 2002-2025 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.crypto.password; + +import org.junit.jupiter.api.BeforeEach; + +/** + * Test {@link AbstractPasswordEncoder} (not intended to be extended). + * + * @author Rob Winch + * @see AbstractPasswordEncoderValidationTests + */ +final class AbstractPasswordEncoderTests extends AbstractPasswordEncoderValidationTests { + + @BeforeEach + void setup() { + setEncoder(new AbstractPasswordEncoder() { + + @Override + protected byte[] encodedNonNullPassword(CharSequence rawPassword, byte[] salt) { + return new byte[0]; + } + }); + } + +} diff --git a/crypto/src/test/java/org/springframework/security/crypto/password/AbstractPasswordEncoderValidationTests.java b/crypto/src/test/java/org/springframework/security/crypto/password/AbstractPasswordEncoderValidationTests.java new file mode 100644 index 0000000000..2a8c2bf913 --- /dev/null +++ b/crypto/src/test/java/org/springframework/security/crypto/password/AbstractPasswordEncoderValidationTests.java @@ -0,0 +1,70 @@ +/* + * Copyright 2002-2025 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.crypto.password; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * A base class for other tests to perform validation of the arguments to + * {@link PasswordEncoder} instances in a consistent way. + * + * @author Rob Winch + */ +public abstract class AbstractPasswordEncoderValidationTests { + + private PasswordEncoder encoder; + + protected void setEncoder(PasswordEncoder encoder) { + this.encoder = encoder; + } + + protected T getEncoder(Class clazz) { + return getEncoder(); + } + + protected T getEncoder() { + return (T) this.encoder; + } + + @Test + void encodeWhenNullThenNull() { + assertThat(this.encoder.encode(null)).isNull(); + } + + @Test + void matchesWhenEncodedPasswordNullThenFalse() { + assertThat(this.encoder.matches("raw", null)).isFalse(); + } + + @Test + void matchesWhenEncodedPasswordEmptyThenFalse() { + assertThat(this.encoder.matches("raw", "")).isFalse(); + } + + @Test + void matchesWhenRawPasswordNullThenFalse() { + assertThat(this.encoder.matches(null, this.encoder.encode("password"))).isFalse(); + } + + @Test + void matchesWhenRawPasswordEmptyThenFalse() { + assertThat(this.encoder.matches("", this.encoder.encode("password"))).isFalse(); + } + +} diff --git a/crypto/src/test/java/org/springframework/security/crypto/password/AbstractValidatingPasswordEncoderTests.java b/crypto/src/test/java/org/springframework/security/crypto/password/AbstractValidatingPasswordEncoderTests.java new file mode 100644 index 0000000000..fc3576becf --- /dev/null +++ b/crypto/src/test/java/org/springframework/security/crypto/password/AbstractValidatingPasswordEncoderTests.java @@ -0,0 +1,43 @@ +/* + * Copyright 2002-2025 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.crypto.password; + +import org.junit.jupiter.api.BeforeEach; + +/** + * Test {@link AbstractValidatingPasswordEncoder}. + * + * @author Rob Winch + */ +class AbstractValidatingPasswordEncoderTests extends AbstractPasswordEncoderValidationTests { + + @BeforeEach + void setup() { + setEncoder(new AbstractValidatingPasswordEncoder() { + @Override + protected String encodeNonNullPassword(String rawPassword) { + return ""; + } + + @Override + protected boolean matchesNonNull(String rawPassword, String encodedPassword) { + return false; + } + }); + } + +} diff --git a/crypto/src/test/java/org/springframework/security/crypto/password/DelegatingPasswordEncoderTests.java b/crypto/src/test/java/org/springframework/security/crypto/password/DelegatingPasswordEncoderTests.java index 2ac32d8a53..ea17cf3a32 100644 --- a/crypto/src/test/java/org/springframework/security/crypto/password/DelegatingPasswordEncoderTests.java +++ b/crypto/src/test/java/org/springframework/security/crypto/password/DelegatingPasswordEncoderTests.java @@ -41,7 +41,7 @@ import static org.mockito.Mockito.verifyNoMoreInteractions; * @since 5.0 */ @ExtendWith(MockitoExtension.class) -public class DelegatingPasswordEncoderTests { +public class DelegatingPasswordEncoderTests extends AbstractPasswordEncoderValidationTests { @Mock private PasswordEncoder bcrypt; @@ -64,8 +64,6 @@ public class DelegatingPasswordEncoderTests { private Map delegates; - private DelegatingPasswordEncoder passwordEncoder; - private DelegatingPasswordEncoder onlySuffixPasswordEncoder; private static final String NO_PASSWORD_ENCODER_MAPPED = "There is no password encoder mapped for the id 'unmapped'. " @@ -81,8 +79,7 @@ public class DelegatingPasswordEncoderTests { this.delegates = new HashMap<>(); this.delegates.put(this.bcryptId, this.bcrypt); this.delegates.put("noop", this.noop); - this.passwordEncoder = new DelegatingPasswordEncoder(this.bcryptId, this.delegates); - + setEncoder(new DelegatingPasswordEncoder(this.bcryptId, this.delegates)); this.onlySuffixPasswordEncoder = new DelegatingPasswordEncoder(this.bcryptId, this.delegates, "", "$"); } @@ -149,14 +146,14 @@ public class DelegatingPasswordEncoderTests { @Test public void setDefaultPasswordEncoderForMatchesWhenNullThenIllegalArgumentException() { assertThatIllegalArgumentException() - .isThrownBy(() -> this.passwordEncoder.setDefaultPasswordEncoderForMatches(null)); + .isThrownBy(() -> getEncoder(DelegatingPasswordEncoder.class).setDefaultPasswordEncoderForMatches(null)); } @Test public void matchesWhenCustomDefaultPasswordEncoderForMatchesThenDelegates() { String encodedPassword = "{unmapped}" + this.rawPassword; - this.passwordEncoder.setDefaultPasswordEncoderForMatches(this.invalidId); - assertThat(this.passwordEncoder.matches(this.rawPassword, encodedPassword)).isFalse(); + getEncoder(DelegatingPasswordEncoder.class).setDefaultPasswordEncoderForMatches(this.invalidId); + assertThat(getEncoder().matches(this.rawPassword, encodedPassword)).isFalse(); verify(this.invalidId).matches(this.rawPassword, encodedPassword); verifyNoMoreInteractions(this.bcrypt, this.noop); } @@ -164,7 +161,7 @@ public class DelegatingPasswordEncoderTests { @Test public void encodeWhenValidThenUsesIdForEncode() { given(this.bcrypt.encode(this.rawPassword)).willReturn(this.encodedPassword); - assertThat(this.passwordEncoder.encode(this.rawPassword)).isEqualTo(this.bcryptEncodedPassword); + assertThat(getEncoder().encode(this.rawPassword)).isEqualTo(this.bcryptEncodedPassword); } @Test @@ -176,7 +173,7 @@ public class DelegatingPasswordEncoderTests { @Test public void matchesWhenBCryptThenDelegatesToBCrypt() { given(this.bcrypt.matches(this.rawPassword, this.encodedPassword)).willReturn(true); - assertThat(this.passwordEncoder.matches(this.rawPassword, this.bcryptEncodedPassword)).isTrue(); + assertThat(getEncoder().matches(this.rawPassword, this.bcryptEncodedPassword)).isTrue(); verify(this.bcrypt).matches(this.rawPassword, this.encodedPassword); verifyNoMoreInteractions(this.noop); } @@ -192,7 +189,7 @@ public class DelegatingPasswordEncoderTests { @Test public void matchesWhenNoopThenDelegatesToNoop() { given(this.noop.matches(this.rawPassword, this.encodedPassword)).willReturn(true); - assertThat(this.passwordEncoder.matches(this.rawPassword, this.noopEncodedPassword)).isTrue(); + assertThat(getEncoder().matches(this.rawPassword, this.noopEncodedPassword)).isTrue(); verify(this.noop).matches(this.rawPassword, this.encodedPassword); verifyNoMoreInteractions(this.bcrypt); } @@ -200,7 +197,7 @@ public class DelegatingPasswordEncoderTests { @Test public void matchesWhenUnMappedThenIllegalArgumentException() { assertThatIllegalArgumentException() - .isThrownBy(() -> this.passwordEncoder.matches(this.rawPassword, "{unmapped}" + this.rawPassword)) + .isThrownBy(() -> getEncoder().matches(this.rawPassword, "{unmapped}" + this.rawPassword)) .withMessage(NO_PASSWORD_ENCODER_MAPPED); verifyNoMoreInteractions(this.bcrypt, this.noop); } @@ -208,7 +205,7 @@ public class DelegatingPasswordEncoderTests { @Test public void matchesWhenNoClosingPrefixStringThenIllegalArgumentException() { assertThatIllegalArgumentException() - .isThrownBy(() -> this.passwordEncoder.matches(this.rawPassword, "{bcrypt" + this.rawPassword)) + .isThrownBy(() -> getEncoder().matches(this.rawPassword, "{bcrypt" + this.rawPassword)) .withMessage(MALFORMED_PASSWORD_ENCODER_PREFIX); verifyNoMoreInteractions(this.bcrypt, this.noop); } @@ -216,7 +213,7 @@ public class DelegatingPasswordEncoderTests { @Test public void matchesWhenNoStartingPrefixStringThenFalse() { assertThatIllegalArgumentException() - .isThrownBy(() -> this.passwordEncoder.matches(this.rawPassword, "bcrypt}" + this.rawPassword)) + .isThrownBy(() -> getEncoder().matches(this.rawPassword, "bcrypt}" + this.rawPassword)) .withMessage(MALFORMED_PASSWORD_ENCODER_PREFIX); verifyNoMoreInteractions(this.bcrypt, this.noop); } @@ -224,7 +221,7 @@ public class DelegatingPasswordEncoderTests { @Test public void matchesWhenNoIdStringThenFalse() { assertThatIllegalArgumentException() - .isThrownBy(() -> this.passwordEncoder.matches(this.rawPassword, "{}" + this.rawPassword)) + .isThrownBy(() -> getEncoder().matches(this.rawPassword, "{}" + this.rawPassword)) .withMessage(MALFORMED_PASSWORD_ENCODER_PREFIX); verifyNoMoreInteractions(this.bcrypt, this.noop); } @@ -232,7 +229,7 @@ public class DelegatingPasswordEncoderTests { @Test public void matchesWhenPrefixInMiddleThenFalse() { assertThatIllegalArgumentException() - .isThrownBy(() -> this.passwordEncoder.matches(this.rawPassword, "invalid" + this.bcryptEncodedPassword)) + .isThrownBy(() -> getEncoder().matches(this.rawPassword, "invalid" + this.bcryptEncodedPassword)) .isInstanceOf(IllegalArgumentException.class) .withMessage(MALFORMED_PASSWORD_ENCODER_PREFIX); verifyNoMoreInteractions(this.bcrypt, this.noop); @@ -251,56 +248,51 @@ public class DelegatingPasswordEncoderTests { @Test public void matchesWhenNullIdThenDelegatesToInvalidId() { this.delegates.put(null, this.invalidId); - this.passwordEncoder = new DelegatingPasswordEncoder(this.bcryptId, this.delegates); + setEncoder(new DelegatingPasswordEncoder(this.bcryptId, this.delegates)); given(this.invalidId.matches(this.rawPassword, this.encodedPassword)).willReturn(true); - assertThat(this.passwordEncoder.matches(this.rawPassword, this.encodedPassword)).isTrue(); + assertThat(getEncoder().matches(this.rawPassword, this.encodedPassword)).isTrue(); verify(this.invalidId).matches(this.rawPassword, this.encodedPassword); verifyNoMoreInteractions(this.bcrypt, this.noop); } @Test - public void matchesWhenRawPasswordNotNullAndEncodedPasswordNullThenThrowsIllegalArgumentException() { - assertThatIllegalArgumentException().isThrownBy(() -> this.passwordEncoder.matches(this.rawPassword, null)); - } - - @Test - public void upgradeEncodingWhenEncodedPasswordNullThenTrue() { - assertThat(this.passwordEncoder.upgradeEncoding(null)).isTrue(); + public void upgradeEncodingWhenEncodedPasswordNullThenFalse() { + assertThat(getEncoder().upgradeEncoding(null)).isFalse(); } @Test public void upgradeEncodingWhenNullIdThenTrue() { - assertThat(this.passwordEncoder.upgradeEncoding(this.encodedPassword)).isTrue(); + assertThat(getEncoder().upgradeEncoding(this.encodedPassword)).isTrue(); } @Test public void upgradeEncodingWhenIdInvalidFormatThenTrue() { - assertThat(this.passwordEncoder.upgradeEncoding("{bcrypt" + this.encodedPassword)).isTrue(); + assertThat(getEncoder().upgradeEncoding("{bcrypt" + this.encodedPassword)).isTrue(); } @Test public void upgradeEncodingWhenSameIdAndEncoderFalseThenEncoderDecidesFalse() { - assertThat(this.passwordEncoder.upgradeEncoding(this.bcryptEncodedPassword)).isFalse(); + assertThat(getEncoder().upgradeEncoding(this.bcryptEncodedPassword)).isFalse(); verify(this.bcrypt).upgradeEncoding(this.encodedPassword); } @Test public void upgradeEncodingWhenSameIdAndEncoderTrueThenEncoderDecidesTrue() { given(this.bcrypt.upgradeEncoding(any())).willReturn(true); - assertThat(this.passwordEncoder.upgradeEncoding(this.bcryptEncodedPassword)).isTrue(); + assertThat(getEncoder().upgradeEncoding(this.bcryptEncodedPassword)).isTrue(); verify(this.bcrypt).upgradeEncoding(this.encodedPassword); } @Test public void upgradeEncodingWhenDifferentIdThenTrue() { - assertThat(this.passwordEncoder.upgradeEncoding(this.noopEncodedPassword)).isTrue(); + assertThat(getEncoder().upgradeEncoding(this.noopEncodedPassword)).isTrue(); verifyNoMoreInteractions(this.bcrypt); } @Test void matchesShouldThrowIllegalArgumentExceptionWhenNoPasswordEncoderIsMappedForTheId() { assertThatIllegalArgumentException() - .isThrownBy(() -> this.passwordEncoder.matches("rawPassword", "prefixEncodedPassword")) + .isThrownBy(() -> getEncoder().matches("rawPassword", "prefixEncodedPassword")) .isInstanceOf(IllegalArgumentException.class) .withMessage(NO_PASSWORD_ENCODER_PREFIX); verifyNoMoreInteractions(this.bcrypt, this.noop); diff --git a/crypto/src/test/java/org/springframework/security/crypto/password/LdapShaPasswordEncoderTests.java b/crypto/src/test/java/org/springframework/security/crypto/password/LdapShaPasswordEncoderTests.java index 500724129e..378a3d3a62 100644 --- a/crypto/src/test/java/org/springframework/security/crypto/password/LdapShaPasswordEncoderTests.java +++ b/crypto/src/test/java/org/springframework/security/crypto/password/LdapShaPasswordEncoderTests.java @@ -16,6 +16,7 @@ package org.springframework.security.crypto.password; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.security.crypto.keygen.KeyGenerators; @@ -29,19 +30,22 @@ import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException * @author Luke Taylor */ @SuppressWarnings("deprecation") -public class LdapShaPasswordEncoderTests { +public class LdapShaPasswordEncoderTests extends AbstractPasswordEncoderValidationTests { - LdapShaPasswordEncoder sha = new LdapShaPasswordEncoder(); + @BeforeEach + void setup() { + setEncoder(new LdapShaPasswordEncoder()); + } @Test public void invalidPasswordFails() { - assertThat(this.sha.matches("wrongpassword", "{SHA}ddSFGmjXYPbZC+NXR2kCzBRjqiE=")).isFalse(); + assertThat(getEncoder().matches("wrongpassword", "{SHA}ddSFGmjXYPbZC+NXR2kCzBRjqiE=")).isFalse(); } @Test public void invalidSaltedPasswordFails() { - assertThat(this.sha.matches("wrongpassword", "{SSHA}25ro4PKC8jhQZ26jVsozhX/xaP0suHgX")).isFalse(); - assertThat(this.sha.matches("wrongpassword", "{SSHA}PQy2j+6n5ytA+YlAKkM8Fh4p6u2JxfVd")).isFalse(); + assertThat(getEncoder().matches("wrongpassword", "{SSHA}25ro4PKC8jhQZ26jVsozhX/xaP0suHgX")).isFalse(); + assertThat(getEncoder().matches("wrongpassword", "{SSHA}PQy2j+6n5ytA+YlAKkM8Fh4p6u2JxfVd")).isFalse(); } /** @@ -49,12 +53,13 @@ public class LdapShaPasswordEncoderTests { */ @Test public void validPasswordSucceeds() { - this.sha.setForceLowerCasePrefix(false); - assertThat(this.sha.matches("boabspasswurd", "{SHA}ddSFGmjXYPbZC+NXR2kCzBRjqiE=")).isTrue(); - assertThat(this.sha.matches("boabspasswurd", "{sha}ddSFGmjXYPbZC+NXR2kCzBRjqiE=")).isTrue(); - this.sha.setForceLowerCasePrefix(true); - assertThat(this.sha.matches("boabspasswurd", "{SHA}ddSFGmjXYPbZC+NXR2kCzBRjqiE=")).isTrue(); - assertThat(this.sha.matches("boabspasswurd", "{sha}ddSFGmjXYPbZC+NXR2kCzBRjqiE=")).isTrue(); + LdapShaPasswordEncoder ldap = getEncoder(); + ldap.setForceLowerCasePrefix(false); + assertThat(getEncoder().matches("boabspasswurd", "{SHA}ddSFGmjXYPbZC+NXR2kCzBRjqiE=")).isTrue(); + assertThat(getEncoder().matches("boabspasswurd", "{sha}ddSFGmjXYPbZC+NXR2kCzBRjqiE=")).isTrue(); + ldap.setForceLowerCasePrefix(true); + assertThat(getEncoder().matches("boabspasswurd", "{SHA}ddSFGmjXYPbZC+NXR2kCzBRjqiE=")).isTrue(); + assertThat(getEncoder().matches("boabspasswurd", "{sha}ddSFGmjXYPbZC+NXR2kCzBRjqiE=")).isTrue(); } /** @@ -62,47 +67,49 @@ public class LdapShaPasswordEncoderTests { */ @Test public void validSaltedPasswordSucceeds() { - this.sha.setForceLowerCasePrefix(false); - assertThat(this.sha.matches("boabspasswurd", "{SSHA}25ro4PKC8jhQZ26jVsozhX/xaP0suHgX")).isTrue(); - assertThat(this.sha.matches("boabspasswurd", "{ssha}PQy2j+6n5ytA+YlAKkM8Fh4p6u2JxfVd")).isTrue(); - this.sha.setForceLowerCasePrefix(true); - assertThat(this.sha.matches("boabspasswurd", "{SSHA}25ro4PKC8jhQZ26jVsozhX/xaP0suHgX")).isTrue(); - assertThat(this.sha.matches("boabspasswurd", "{ssha}PQy2j+6n5ytA+YlAKkM8Fh4p6u2JxfVd")).isTrue(); + LdapShaPasswordEncoder ldap = getEncoder(); + ldap.setForceLowerCasePrefix(false); + assertThat(getEncoder().matches("boabspasswurd", "{SSHA}25ro4PKC8jhQZ26jVsozhX/xaP0suHgX")).isTrue(); + assertThat(getEncoder().matches("boabspasswurd", "{ssha}PQy2j+6n5ytA+YlAKkM8Fh4p6u2JxfVd")).isTrue(); + ldap.setForceLowerCasePrefix(true); + assertThat(getEncoder().matches("boabspasswurd", "{SSHA}25ro4PKC8jhQZ26jVsozhX/xaP0suHgX")).isTrue(); + assertThat(getEncoder().matches("boabspasswurd", "{ssha}PQy2j+6n5ytA+YlAKkM8Fh4p6u2JxfVd")).isTrue(); } @Test // SEC-1031 public void fullLengthOfHashIsUsedInComparison() { - assertThat(this.sha.matches("boabspasswurd", "{SSHA}25ro4PKC8jhQZ26jVsozhX/xaP0suHgX")).isTrue(); + assertThat(getEncoder().matches("boabspasswurd", "{SSHA}25ro4PKC8jhQZ26jVsozhX/xaP0suHgX")).isTrue(); // Change the first hash character from '2' to '3' - assertThat(this.sha.matches("boabspasswurd", "{SSHA}35ro4PKC8jhQZ26jVsozhX/xaP0suHgX")).isFalse(); + assertThat(getEncoder().matches("boabspasswurd", "{SSHA}35ro4PKC8jhQZ26jVsozhX/xaP0suHgX")).isFalse(); // Change the last hash character from 'X' to 'Y' - assertThat(this.sha.matches("boabspasswurd", "{SSHA}25ro4PKC8jhQZ26jVsozhX/xaP0suHgY")).isFalse(); + assertThat(getEncoder().matches("boabspasswurd", "{SSHA}25ro4PKC8jhQZ26jVsozhX/xaP0suHgY")).isFalse(); } @Test public void correctPrefixCaseIsUsed() { - this.sha.setForceLowerCasePrefix(false); - assertThat(this.sha.encode("somepassword").startsWith("{SSHA}")); - this.sha.setForceLowerCasePrefix(true); - assertThat(this.sha.encode("somepassword").startsWith("{ssha}")); - this.sha = new LdapShaPasswordEncoder(KeyGenerators.shared(0)); - this.sha.setForceLowerCasePrefix(false); - assertThat(this.sha.encode("somepassword").startsWith("{SHA}")); - this.sha.setForceLowerCasePrefix(true); - assertThat(this.sha.encode("somepassword").startsWith("{SSHA}")); + LdapShaPasswordEncoder ldap = getEncoder(); + ldap.setForceLowerCasePrefix(false); + assertThat(ldap.encode("somepassword").startsWith("{SSHA}")); + ldap.setForceLowerCasePrefix(true); + assertThat(ldap.encode("somepassword").startsWith("{ssha}")); + setEncoder(new LdapShaPasswordEncoder(KeyGenerators.shared(0))); + ldap.setForceLowerCasePrefix(false); + assertThat(getEncoder().encode("somepassword").startsWith("{SHA}")); + ldap.setForceLowerCasePrefix(true); + assertThat(getEncoder().encode("somepassword").startsWith("{SSHA}")); } @Test public void invalidPrefixIsRejected() { - assertThatIllegalArgumentException().isThrownBy(() -> this.sha.matches("somepassword", "{MD9}xxxxxxxxxx")); + assertThatIllegalArgumentException().isThrownBy(() -> getEncoder().matches("somepassword", "{MD9}xxxxxxxxxx")); } @Test public void malformedPrefixIsRejected() { // No right brace assertThatIllegalArgumentException() - .isThrownBy(() -> this.sha.matches("somepassword", "{SSHA25ro4PKC8jhQZ26jVsozhX/xaP0suHgX")); + .isThrownBy(() -> getEncoder().matches("somepassword", "{SSHA25ro4PKC8jhQZ26jVsozhX/xaP0suHgX")); } } diff --git a/crypto/src/test/java/org/springframework/security/crypto/password/Md4PasswordEncoderTests.java b/crypto/src/test/java/org/springframework/security/crypto/password/Md4PasswordEncoderTests.java index fa4bbfd202..7226564929 100644 --- a/crypto/src/test/java/org/springframework/security/crypto/password/Md4PasswordEncoderTests.java +++ b/crypto/src/test/java/org/springframework/security/crypto/password/Md4PasswordEncoderTests.java @@ -16,59 +16,58 @@ package org.springframework.security.crypto.password; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import static org.assertj.core.api.Assertions.assertThat; @SuppressWarnings("deprecation") -public class Md4PasswordEncoderTests { +public class Md4PasswordEncoderTests extends AbstractPasswordEncoderValidationTests { + + @BeforeEach + void setup() { + setEncoder(new Md4PasswordEncoder()); + } @Test - public void testEncodeUnsaltedPassword() { - Md4PasswordEncoder md4 = new Md4PasswordEncoder(); - md4.setEncodeHashAsBase64(true); - assertThat(md4.matches("ww_uni123", "8zobtq72iAt0W6KNqavGwg==")).isTrue(); + public void matchesWhenEncodedPasswordNullThenFalse() { + assertThat(getEncoder().matches("raw", null)).isFalse(); } @Test - public void testEncodeSaltedPassword() { - Md4PasswordEncoder md4 = new Md4PasswordEncoder(); - md4.setEncodeHashAsBase64(true); - assertThat(md4.matches("ww_uni123", "{Alan K Stewart}ZplT6P5Kv6Rlu6W4FIoYNA==")).isTrue(); + public void matchesWhenEncodedPasswordEmptyThenFalse() { + assertThat(getEncoder().matches("raw", "")).isFalse(); } @Test - public void testEncodeNullPassword() { - Md4PasswordEncoder md4 = new Md4PasswordEncoder(); + public void testEncodeUnsaltedPassword() { + Md4PasswordEncoder md4 = getEncoder(); md4.setEncodeHashAsBase64(true); - assertThat(md4.matches(null, "MdbP4NFq6TG3PFnX4MCJwA==")).isTrue(); + assertThat(md4.matches("ww_uni123", "8zobtq72iAt0W6KNqavGwg==")).isTrue(); } @Test - public void testEncodeEmptyPassword() { - Md4PasswordEncoder md4 = new Md4PasswordEncoder(); + public void testEncodeSaltedPassword() { + Md4PasswordEncoder md4 = getEncoder(); md4.setEncodeHashAsBase64(true); - assertThat(md4.matches(null, "MdbP4NFq6TG3PFnX4MCJwA==")).isTrue(); + assertThat(md4.matches("ww_uni123", "{Alan K Stewart}ZplT6P5Kv6Rlu6W4FIoYNA==")).isTrue(); } @Test public void testNonAsciiPasswordHasCorrectHash() { - Md4PasswordEncoder md4 = new Md4PasswordEncoder(); - assertThat(md4.matches("\u4F60\u597d", "a7f1196539fd1f85f754ffd185b16e6e")).isTrue(); + assertThat(getEncoder().matches("\u4F60\u597d", "a7f1196539fd1f85f754ffd185b16e6e")).isTrue(); } @Test public void testEncodedMatches() { String rawPassword = "password"; - Md4PasswordEncoder md4 = new Md4PasswordEncoder(); - String encodedPassword = md4.encode(rawPassword); - assertThat(md4.matches(rawPassword, encodedPassword)).isTrue(); + String encodedPassword = getEncoder().encode(rawPassword); + assertThat(getEncoder().matches(rawPassword, encodedPassword)).isTrue(); } @Test public void javadocWhenHasSaltThenMatches() { - Md4PasswordEncoder encoder = new Md4PasswordEncoder(); - assertThat(encoder.matches("password", "{thisissalt}6cc7924dad12ade79dfb99e424f25260")); + assertThat(getEncoder().matches("password", "{thisissalt}6cc7924dad12ade79dfb99e424f25260")); } } diff --git a/crypto/src/test/java/org/springframework/security/crypto/password/MessageDigestPasswordEncoderTests.java b/crypto/src/test/java/org/springframework/security/crypto/password/MessageDigestPasswordEncoderTests.java index ec25894a2d..19e7dd78fb 100644 --- a/crypto/src/test/java/org/springframework/security/crypto/password/MessageDigestPasswordEncoderTests.java +++ b/crypto/src/test/java/org/springframework/security/crypto/password/MessageDigestPasswordEncoderTests.java @@ -16,6 +16,7 @@ package org.springframework.security.crypto.password; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import static org.assertj.core.api.Assertions.assertThat; @@ -32,70 +33,69 @@ import static org.assertj.core.api.Assertions.assertThatIllegalStateException; * @author Luke Taylor */ @SuppressWarnings("deprecation") -public class MessageDigestPasswordEncoderTests { +public class MessageDigestPasswordEncoderTests extends AbstractPasswordEncoderValidationTests { + + @BeforeEach + void setup() { + setEncoder(new MessageDigestPasswordEncoder("MD5")); + } @Test public void md5BasicFunctionality() { - MessageDigestPasswordEncoder pe = new MessageDigestPasswordEncoder("MD5"); String raw = "abc123"; - assertThat(pe.matches(raw, "{THIS_IS_A_SALT}a68aafd90299d0b137de28fb4bb68573")).isTrue(); + assertThat(getEncoder().matches(raw, "{THIS_IS_A_SALT}a68aafd90299d0b137de28fb4bb68573")).isTrue(); } @Test public void md5NonAsciiPasswordHasCorrectHash() { - MessageDigestPasswordEncoder pe = new MessageDigestPasswordEncoder("MD5"); // $ echo -n "??" | md5 // 7eca689f0d3389d9dea66ae112e5cfd7 - assertThat(pe.matches("\u4F60\u597d", "7eca689f0d3389d9dea66ae112e5cfd7")).isTrue(); + assertThat(getEncoder().matches("\u4F60\u597d", "7eca689f0d3389d9dea66ae112e5cfd7")).isTrue(); } @Test public void md5Base64() { - MessageDigestPasswordEncoder pe = new MessageDigestPasswordEncoder("MD5"); + MessageDigestPasswordEncoder pe = getEncoder(); pe.setEncodeHashAsBase64(true); - assertThat(pe.matches("abc123", "{THIS_IS_A_SALT}poqv2QKZ0LE33ij7S7aFcw==")).isTrue(); + assertThat(getEncoder().matches("abc123", "{THIS_IS_A_SALT}poqv2QKZ0LE33ij7S7aFcw==")).isTrue(); } @Test public void md5StretchFactorIsProcessedCorrectly() { - MessageDigestPasswordEncoder pe = new MessageDigestPasswordEncoder("MD5"); + MessageDigestPasswordEncoder pe = getEncoder(); pe.setIterations(2); // Calculate value using: // echo -n password{salt} | openssl md5 -binary | openssl md5 - assertThat(pe.matches("password", "{salt}eb753fb0c370582b4ee01b30f304b9fc")).isTrue(); + assertThat(getEncoder().matches("password", "{salt}eb753fb0c370582b4ee01b30f304b9fc")).isTrue(); } @Test public void md5MatchesWhenNullSalt() { - MessageDigestPasswordEncoder pe = new MessageDigestPasswordEncoder("MD5"); - assertThat(pe.matches("password", "5f4dcc3b5aa765d61d8327deb882cf99")).isTrue(); + assertThat(getEncoder().matches("password", "5f4dcc3b5aa765d61d8327deb882cf99")).isTrue(); } @Test public void md5MatchesWhenEmptySalt() { - MessageDigestPasswordEncoder pe = new MessageDigestPasswordEncoder("MD5"); - assertThat(pe.matches("password", "{}f1026a66095fc2058c1f8771ed05d6da")).isTrue(); + assertThat(getEncoder().matches("password", "{}f1026a66095fc2058c1f8771ed05d6da")).isTrue(); } @Test public void md5MatchesWhenHasSalt() { - MessageDigestPasswordEncoder pe = new MessageDigestPasswordEncoder("MD5"); - assertThat(pe.matches("password", "{salt}ce421738b1c5540836bdc8ff707f1572")).isTrue(); + assertThat(getEncoder().matches("password", "{salt}ce421738b1c5540836bdc8ff707f1572")).isTrue(); } @Test public void md5EncodeThenMatches() { String rawPassword = "password"; - MessageDigestPasswordEncoder pe = new MessageDigestPasswordEncoder("MD5"); - String encode = pe.encode(rawPassword); - assertThat(pe.matches(rawPassword, encode)).isTrue(); + String encode = getEncoder().encode(rawPassword); + assertThat(getEncoder().matches(rawPassword, encode)).isTrue(); } @Test public void testBasicFunctionality() { - MessageDigestPasswordEncoder pe = new MessageDigestPasswordEncoder("SHA-1"); + setEncoder(new MessageDigestPasswordEncoder("SHA-1")); String raw = "abc123"; - assertThat(pe.matches(raw, "{THIS_IS_A_SALT}b2f50ffcbd3407fe9415c062d55f54731f340d32")); + assertThat(getEncoder().matches(raw, "{THIS_IS_A_SALT}b2f50ffcbd3407fe9415c062d55f54731f340d32")); } @Test @@ -103,14 +103,15 @@ public class MessageDigestPasswordEncoderTests { MessageDigestPasswordEncoder pe = new MessageDigestPasswordEncoder("SHA-1"); pe.setEncodeHashAsBase64(true); String raw = "abc123"; - assertThat(pe.matches(raw, "{THIS_IS_A_SALT}b2f50ffcbd3407fe9415c062d55f54731f340d32")); + assertThat(getEncoder().matches(raw, "{THIS_IS_A_SALT}b2f50ffcbd3407fe9415c062d55f54731f340d32")); } @Test public void test256() { MessageDigestPasswordEncoder pe = new MessageDigestPasswordEncoder("SHA-1"); String raw = "abc123"; - assertThat(pe.matches(raw, "{THIS_IS_A_SALT}4b79b7de23eb23b78cc5ede227d532b8a51f89b2ec166f808af76b0dbedc47d7")); + assertThat(getEncoder().matches(raw, + "{THIS_IS_A_SALT}4b79b7de23eb23b78cc5ede227d532b8a51f89b2ec166f808af76b0dbedc47d7")); } @Test diff --git a/crypto/src/test/java/org/springframework/security/crypto/password/NoOpPasswordEncoderTests.java b/crypto/src/test/java/org/springframework/security/crypto/password/NoOpPasswordEncoderTests.java new file mode 100644 index 0000000000..6fd490a56e --- /dev/null +++ b/crypto/src/test/java/org/springframework/security/crypto/password/NoOpPasswordEncoderTests.java @@ -0,0 +1,33 @@ +/* + * Copyright 2002-2025 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.crypto.password; + +import org.junit.jupiter.api.BeforeEach; + +/** + * Test {@link NoOpPasswordEncoder}. + * + * @author Rob Winch + */ +class NoOpPasswordEncoderTests extends AbstractPasswordEncoderValidationTests { + + @BeforeEach + void setup() { + setEncoder(NoOpPasswordEncoder.getInstance()); + } + +} diff --git a/web/src/main/java/org/springframework/security/web/authentication/password/HaveIBeenPwnedRestApiPasswordChecker.java b/web/src/main/java/org/springframework/security/web/authentication/password/HaveIBeenPwnedRestApiPasswordChecker.java index 248d760947..d280f99ad9 100644 --- a/web/src/main/java/org/springframework/security/web/authentication/password/HaveIBeenPwnedRestApiPasswordChecker.java +++ b/web/src/main/java/org/springframework/security/web/authentication/password/HaveIBeenPwnedRestApiPasswordChecker.java @@ -26,7 +26,6 @@ import java.util.Locale; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; -import org.springframework.lang.NonNull; import org.springframework.security.authentication.password.CompromisedPasswordChecker; import org.springframework.security.authentication.password.CompromisedPasswordDecision; import org.springframework.security.crypto.codec.Hex; @@ -61,8 +60,10 @@ public final class HaveIBeenPwnedRestApiPasswordChecker implements CompromisedPa } @Override - @NonNull public CompromisedPasswordDecision check(String password) { + if (password == null) { + return new CompromisedPasswordDecision(false); + } byte[] hash = this.sha1Digest.digest(password.getBytes(StandardCharsets.UTF_8)); String encoded = new String(Hex.encode(hash)).toUpperCase(Locale.ROOT); String prefix = encoded.substring(0, PREFIX_LENGTH); diff --git a/web/src/main/java/org/springframework/security/web/authentication/password/HaveIBeenPwnedRestApiReactivePasswordChecker.java b/web/src/main/java/org/springframework/security/web/authentication/password/HaveIBeenPwnedRestApiReactivePasswordChecker.java index fd0141d535..099f74e436 100644 --- a/web/src/main/java/org/springframework/security/web/authentication/password/HaveIBeenPwnedRestApiReactivePasswordChecker.java +++ b/web/src/main/java/org/springframework/security/web/authentication/password/HaveIBeenPwnedRestApiReactivePasswordChecker.java @@ -23,6 +23,7 @@ import java.util.Locale; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.Nullable; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import reactor.core.scheduler.Schedulers; @@ -61,9 +62,10 @@ public class HaveIBeenPwnedRestApiReactivePasswordChecker implements ReactiveCom } @Override - public Mono check(String password) { + public Mono check(@Nullable String password) { return getHash(password).map((hash) -> new String(Hex.encode(hash))) .flatMap(this::findLeakedPassword) + .defaultIfEmpty(Boolean.FALSE) .map(CompromisedPasswordDecision::new); } @@ -94,8 +96,9 @@ public class HaveIBeenPwnedRestApiReactivePasswordChecker implements ReactiveCom this.webClient = webClient; } - private Mono getHash(String password) { - return Mono.fromSupplier(() -> this.sha1Digest.digest(password.getBytes(StandardCharsets.UTF_8))) + private Mono getHash(@Nullable String rawPassword) { + return Mono.justOrEmpty(rawPassword) + .map((password) -> this.sha1Digest.digest(password.getBytes(StandardCharsets.UTF_8))) .subscribeOn(Schedulers.boundedElastic()) .publishOn(Schedulers.parallel()); } diff --git a/web/src/test/java/org/springframework/security/web/authentication/password/HaveIBeenPwnedRestApiReactivePasswordCheckerTests.java b/web/src/test/java/org/springframework/security/web/authentication/password/HaveIBeenPwnedRestApiReactivePasswordCheckerTests.java index 7d5550e6d0..1b0f5b7945 100644 --- a/web/src/test/java/org/springframework/security/web/authentication/password/HaveIBeenPwnedRestApiReactivePasswordCheckerTests.java +++ b/web/src/test/java/org/springframework/security/web/authentication/password/HaveIBeenPwnedRestApiReactivePasswordCheckerTests.java @@ -102,4 +102,11 @@ class HaveIBeenPwnedRestApiReactivePasswordCheckerTests { .verifyComplete(); } + @Test + void checkWhenNullThenNotCompromised() { + StepVerifier.create(this.passwordChecker.check(null)) + .assertNext((check) -> assertThat(check.isCompromised()).isFalse()) + .verifyComplete(); + } + }