Spring in Kotlin: from 5.3 to 6.0 security Configuration - spring

I'm facing lots of issues in doing Spring security configurations that I used to have in v5.3 applied in v6.
This is the file I had
#Configuration
#EnableWebSecurity
class WebSecurityConfiguration : WebSecurityConfigurerAdapter() {
#Autowired
lateinit var service: UserService
/**
* Will be resolved into: WebSecurityEntryPoint injected instance.
*/
#Autowired
lateinit var unauthorizedHandler: AuthenticationEntryPoint
#Autowired
lateinit var successHandler: WebSecurityAuthSuccessHandler
#Autowired
override fun configure(auth: AuthenticationManagerBuilder) {
auth.authenticationProvider(authenticationProvider())
}
override fun configure(http: HttpSecurity?) {
http
?.csrf()?.disable()
?.exceptionHandling()
?.authenticationEntryPoint(unauthorizedHandler)
?.and()
?.authorizeRequests()
/**
* Access to Notes and Todos API calls is given to any authenticated system user.
*/
?.antMatchers("/notes")?.authenticated()
?.antMatchers("/notes/**")?.authenticated()
?.antMatchers("/todos")?.authenticated()
?.antMatchers("/todos/**")?.authenticated()
/**
* Access to User API calls is given only to Admin user.
*/
?.antMatchers("/users")?.hasAnyAuthority("ADMIN")
?.antMatchers("/users/**")?.hasAnyAuthority("ADMIN")
?.and()
?.formLogin()
?.successHandler(successHandler)
?.failureHandler(SimpleUrlAuthenticationFailureHandler())
?.and()
?.logout()
}
#Bean
fun authenticationProvider(): DaoAuthenticationProvider {
val authProvider = DaoAuthenticationProvider()
authProvider.setUserDetailsService(service)
authProvider.setPasswordEncoder(encoder())
return authProvider
}
#Bean
fun encoder(): PasswordEncoder = BCryptPasswordEncoder(11)
#Bean
fun accessDecisionManager(): AccessDecisionManager {
val decisionVoters = Arrays.asList(
WebExpressionVoter(),
RoleVoter(),
AuthenticatedVoter()
)
return UnanimousBased(decisionVoters)
}
}
I used the documentation in Spring.io
https://spring.io/blog/2022/02/21/spring-security-without-the-websecurityconfigureradapter
and I'm just hitting the wall since then. their documentation is not helpful and the new dependencies aren't working the same.
how can this be done now?
P.S: I often keep getting this error:
Caused by: java.lang.ClassNotFoundException: org.springframework.security.core.context.DeferredSecurityContext
which i couldn't find anywhere

Okey... I managed to solve it this way
first I had to add the security dependency for v6
implementation("org.springframework.security:spring-security-core:6.0.1")
and I made the Security Configuration this way
#Configuration
#EnableWebSecurity
class SecurityConfiguration(
private val userService: UserService,
private val unauthorizedHandler: AuthenticationEntryPoint,
private val successHandler: WebSecurityAuthSuccessHandler
) {
/**
* Will be resolved into: WebSecurityEntryPoint injected instance.
*/
#Bean
fun myPasswordEncoder(): PasswordEncoder {
return BCryptPasswordEncoder(11)
}
#Primary
fun configureAuthentication(auth: AuthenticationManagerBuilder): AuthenticationManagerBuilder {
return auth.authenticationProvider(authenticationProvider())
}
#Bean
fun authenticationProvider(): DaoAuthenticationProvider {
val authProvider = DaoAuthenticationProvider()
authProvider.setUserDetailsService(userService)
authProvider.setPasswordEncoder(myPasswordEncoder())
return authProvider
}
#Bean
fun accessDecisionManager(): AccessDecisionManager {
val decisionVoter = listOf(
WebExpressionVoter(),
RoleVoter(),
AuthenticatedVoter()
)
return UnanimousBased(decisionVoter)
}
#Bean
fun configureHttpSecurity(httpSecurity: HttpSecurity): SecurityFilterChain {
httpSecurity
.csrf().disable()
.exceptionHandling()
.authenticationEntryPoint(unauthorizedHandler)
.and()
.authorizeHttpRequests()
/**
* Access to Notes and Todos API calls is given to any authenticated system user.
*/
.requestMatchers("/notes").authenticated()
.requestMatchers("/notes/**").authenticated()
.requestMatchers("/todos").authenticated()
.requestMatchers("/todos/**").authenticated()
/**
* Access to User API calls is given only to Admin user.
*/
.requestMatchers("/users").hasAnyAuthority("ADMIN")
.requestMatchers("/users/**").hasAnyAuthority("ADMIN")
.and()
.formLogin()
.successHandler(successHandler)
.failureHandler(SimpleUrlAuthenticationFailureHandler())
.and()
.logout()
return httpSecurity.build()
}
}

Related

What is the replacement for TokenStore, TokenServices and JwtAccessTokenConverter in Spring Security 5

I am upgrading Spring Boot from 2.3.12.RELEASE to 2.7.7 in my Kotlin project and found out that I have to change the code for Spring Security because Spring Security OAuth that was used in this project is no longer supported and has to be migrated to Spring Security 5+. I have this configuration that I want to migrate (some business data omitted):
#Configuration
#EnableResourceServer
#EnableGlobalMethodSecurity(prePostEnabled = true)
class ResourceServerConfig : ResourceServerConfigurerAdapter() {
#Value("...")
private val claimAud: String? = null
#Value("...")
private val urlJwk: String? = null
override fun configure(resources: ResourceServerSecurityConfigurer) {
resources.tokenStore(tokenStore())
resources.resourceId(claimAud)
}
#Bean
fun tokenStore(): TokenStore {
logger.info("JWK settings resource config: $urlJwk")
return JwkTokenStore(urlJwk, createJwtAccessTokenConverter())
}
#Throws(Exception::class)
override fun configure(http: HttpSecurity) {
http.anonymous().and().cors(withDefaults())
.authorizeRequests()
.mvcMatchers(BASE_PATH_PATTERN).permitAll()
.mvcMatchers(API_PATH_PATTERN).permitAll()
.mvcMatchers(ADMIN_PATH_PATTERN).authenticated()
}
#Bean
fun securityEvaluationContextExtension(): SecurityEvaluationContextExtension {
return SecurityEvaluationContextExtension()
}
#Bean
#Primary
fun tokenServices(): DefaultTokenServices {
val defaultTokenServices = DefaultTokenServices()
defaultTokenServices.setTokenStore(tokenStore())
return defaultTokenServices
}
#Bean
fun createJwtAccessTokenConverter(): JwtAccessTokenConverter? {
val converter = JwtAccessTokenConverter()
converter.accessTokenConverter = MyTokenConverter()
return converter
}
#Component
class MyTokenConverter : DefaultAccessTokenConverter(), JwtAccessTokenConverterConfigurer {
override fun extractAuthentication(claims: Map<String?, *>?): OAuth2Authentication {
val authentication = super.extractAuthentication(claims)
authentication.details = claims
return authentication
}
override fun configure(converter: JwtAccessTokenConverter) {
converter.accessTokenConverter = this
}
}
I don't know what to do with those methods related to TokenStore, TokenServices or TokenConverter, how to replace them.
I consulted the migrtation guide https://github.com/spring-projects/spring-security/wiki/OAuth-2.0-Migration-Guide but it seems that it lacks a lot of information, there is no specific guide anywhere actually for how to replace those components that I mentioned in my question.

Can not get user info with Spring Security SAML WITHOUT Spring Boot

I´m working on SAML integration in an older project but I can´t get the user information.
I've guided me with the response of this question:
https://stackoverflow.com/questions/70275050/spring-security-saml-identity-metadata-without-spring-boot
The project has these versions:
spring framework 5.3.24
spring security 5.6.10
opensaml 3.4.6
This is my code:
#Configuration
public class SAMLSecurityConfig {
private static final String URL_METADATA = "https://auth-dev.mycompany.com/app/id/sso/saml/metadata";
#Bean("samlRegistration")
public RelyingPartyRegistrationRepository relyingPartyRegistrationRepository() {
RelyingPartyRegistration relyingPartyRegistration = RelyingPartyRegistrations.fromMetadataLocation(URL_METADATA)
.registrationId("id")
.build();
return new InMemoryRelyingPartyRegistrationRepository(relyingPartyRegistration);
}
}
#EnableWebSecurity
public class WebSecurity {
#Configuration
#Order(2)
public static class SAMLSecurityFilter extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity httpSecurity) throws Exception {
httpSecurity.saml2Login(Customizer.withDefaults())
.antMatcher("/login/assertion")
.authorizeRequests()
.anyRequest()
.authenticated();
}
}
}
#Controller("loginController")
public class BoCRLoginController {
#RequestMapping(value = "/login/assertion", method = {RequestMethod.POST},
consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE, produces = MediaType.APPLICATION_XML_VALUE)
public ResponseEntity<String> assertLoginData(#AuthenticationPrincipal Saml2AuthenticatedPrincipal principal) {
System.out.println(principal); //here I get a null
return new ResponseEntity<>(HttpStatus.OK);
}
}
Once I did the login on okta the class: Saml2AuthenticatedPrincipal comes null value.
Could you help me to know why I received null value on the object Saml2AuthenticatedPrincipal where suppose have to receive the user information?

Spring Security without WebSecurityConfiguererAdapter, register two AuthenticationProvider

Since the deprication of WebSecurityConfiguererAdapter, I am not sure on how to implement my two custom AuthenticationProvider classes. Everything works in case I only use one of the AuthenticationProviders, but having two prevents both of them to function. I tried giving them #Order annotations but that doesn't work either. How would I register my custom AuthenticationProviders to make them work along each other and being called when the class defined in its support method matches. By the way, I'am a SpringBoot noob ;)
#Component
class AuthenticationService(
private val authenticationManager: AuthenticationManager,
) {
fun login(loginDto: LoginDto): Boolean {
authenticationManager.authenticate(
Token2(
loginDto.username, loginDto.password
)
)
return true
}
}
#Configuration
class ProdSecurityConfiguration {
#Bean
fun passwordEncoder(): PasswordEncoder {
return BCryptPasswordEncoder()
}
#Bean
fun authenticationManager(authenticationConfiguration: AuthenticationConfiguration): AuthenticationManager {
return authenticationConfiguration.authenticationManager
}
#Bean
fun filterChain(http: HttpSecurity): SecurityFilterChain {
http.csrf().disable().cors().disable()
http.authorizeRequests()
.antMatchers("/login", "/register").permitAll().anyRequest().authenticated().and().sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
return http.build()
}
}
#Component
class CredentialsAuthProvider : AuthenticationProvider {
override fun authenticate(authentication: Authentication): Authentication {
return UsernamePasswordAuthenticationToken(
"myUser#mailbox.org", "password"
)
}
override fun supports(authentication: Class<*>?): Boolean {
return authentication == Token1::class.java
}
}
#Component
class CredentialsAuthProvider2 : AuthenticationProvider {
override fun authenticate(authentication: Authentication): Authentication {
return UsernamePasswordAuthenticationToken(
"myUser#mailbox.org", "password"
)
}
override fun supports(authentication: Class<*>?): Boolean {
return authentication == Token2::class.java
}
}
I removed the #Component annotation from my custom AuthenticationProviders, I created #Beans in my SecurityConfiguration class for each custom AuthenticationProvider where I directly call their constructor instead of using springs DI. Then used them within my AthenticationManager Bean where I return a ProviderManager.
Code snippets for explanation:
#Configuration
class ProdSecurityConfiguration {
#Bean
fun credentialsAuthProvider(): AuthenticationProvider {
return CredentialsAuthProvider()
}
#Bean
fun credentialsAuthProvider2(): AuthenticationProvider {
return CredentialsAuthProvider2()
}
#Bean
fun authenticationManager(): AuthenticationManager {
return ProviderManager(listOf(credentialsAuthProvider(), credentialsAuthProvider2()))
}
#Bean
fun filterChain(http: HttpSecurity): SecurityFilterChain {
http.csrf().disable().cors().disable()
http.authorizeRequests()
.antMatchers("/login", "/register").permitAll().anyRequest().authenticated().and().sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
return http.build()
}
}

Spring Security Google OAuth2 authorizationUri became domain after deployed on aws

So I am current testing with spring securtiy with google oauth.
it works fine, when trying to login with google through
/oauth2/authorization/google
on localhost
but when i deployed the application on tomcat on aws ec2 instance,
the authorizationuri became sometime like below
when running on localhost:
https://accounts.google.com/o/oauth2/v2/auth/oauthchooseaccount? ............
when running on aws ec2 domain:
http://{domain}:{port}/o/oauth2/v2/auth? ..........
but when i manually replace the http domain and port with https://accounts.google.com/
it directs me to google login page and are able to complete the login successfully.
So i wonder if there is any part of the setting ive being missing or did wrong that cause it to happen.
Thanks in advance.
the complete code for spring security setting is like below
#Configuration
#EnableWebSecurity
class SecurityConfig : WebSecurityConfigurerAdapter() {
#Autowired
private lateinit var oidUserSer:OidUserService
#Bean
fun clientRegistrationRepository(): ClientRegistrationRepository {
return InMemoryClientRegistrationRepository(googleClientRegistration())
}
private fun googleClientRegistration(): ClientRegistration {
return ClientRegistration.withRegistrationId("google")
.clientId("clientId")
.clientSecret("secret")
.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
.redirectUri("https://{domain:port}/login/oauth2/code/{registrationId}")
.scope("openid", "profile", "email", "address", "phone")
.authorizationUri("https://accounts.google.com/o/oauth2/v2/auth")
.tokenUri("https://www.googleapis.com/oauth2/v4/token")
.userInfoUri("https://www.googleapis.com/oauth2/v3/userinfo")
.userNameAttributeName(IdTokenClaimNames.SUB)
.jwkSetUri("https://www.googleapis.com/oauth2/v3/certs")
.clientName("Google")
.build()
}
#Bean
fun customAuthorizationRequestResolver(): CustomAuthorizationRequestResolver {
val repo = InMemoryClientRegistrationRepository(
CommonOAuth2Provider.GOOGLE.getBuilder("google")
.clientName("Google")
.clientId("61770666483-dj64uabnia7tq2g0kri2ajrb7sl21r3t.apps.googleusercontent.com")
.clientSecret("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")
.redirectUri("https://{domain:port}/login/oauth2/code/{registrationId}")
.build()
)
val baseUri = OAuth2AuthorizationRequestRedirectFilter.DEFAULT_AUTHORIZATION_REQUEST_BASE_URI
val customizeAuthorizationRequest= CustomAuthorizationRequestResolver(repo,baseUri)
return customizeAuthorizationRequest
}
/**
* セキュリティの有効範囲設定
*/
#Override
override fun configure(web: WebSecurity) {
// org.springframework.security.web.firewall.RequestRejectedException:
// The request was rejected because the URL contained a potentially malicious String ";"
// というエラーログがコンソールに出力されるため、下記を追加
val firewall = DefaultHttpFirewall()
web.httpFirewall(firewall)
web.ignoring().antMatchers(
"/img/**",
"/css/**",
"/js/**",
"/libs/**"
)
}
#Override
#Throws(Exception::class)
override fun configure(http: HttpSecurity) {
http.exceptionHandling()
.authenticationEntryPoint(LoginUrlAuthenticationEntryPoint("/login"));
http.oauth2Login()
.loginPage("/login")
.defaultSuccessUrl("/login-success", true)
.userInfoEndpoint().oidcUserService(oidUserSer).
and()
.failureUrl("/login?error")
.authorizationEndpoint()
.authorizationRequestResolver(customAuthorizationRequestResolver())
http.logout()
//.logoutRequestMatcher(AntPathRequestMatcher("/logout**"))
.logoutUrl("/logout")
.logoutSuccessUrl("/login?logout")
.logoutSuccessHandler(CustomLogoutSuccessHandler())
//.invalidateHttpSession(true)
//セッション設定
http.sessionManagement().
invalidSessionUrl("/login?timeout=true")
}
}
class CustomAuthorizationRequestResolver(repo: ClientRegistrationRepository?,
authorizationRequestBaseUri: String?):OAuth2AuthorizationRequestResolver{
private var defaultResolver:DefaultOAuth2AuthorizationRequestResolver? = DefaultOAuth2AuthorizationRequestResolver(repo, authorizationRequestBaseUri)
override fun resolve(request: HttpServletRequest): OAuth2AuthorizationRequest? {
val authorizationRequest:OAuth2AuthorizationRequest? =
this.defaultResolver?.resolve(request)
return customAuthorizationRequest(authorizationRequest)
}
override fun resolve(request: HttpServletRequest?, clientRegistrationId: String?): OAuth2AuthorizationRequest? {
val authorizationRequest = this.defaultResolver?.resolve(
request, clientRegistrationId);
return authorizationRequest?.let { customAuthorizationRequest(it) }
}
private fun customAuthorizationRequest(authorizationRequest: OAuth2AuthorizationRequest?): OAuth2AuthorizationRequest? {
var param:OAuth2AuthorizationRequest? = null
if(authorizationRequest!=null){
val additionalParameter: LinkedHashMap<String,Any> = LinkedHashMap(authorizationRequest!!.additionalParameters)
additionalParameter.put("prompt","select_account+consent")
param = OAuth2AuthorizationRequest.from(authorizationRequest).additionalParameters(additionalParameter).build()
}
return param
}
}

How to extract custom Principal in OAuth2 Resource Server?

I'm using Keycloak as my OAuth2 Authorization Server and I configured an OAuth2 Resource Server for Multitenancy following this official example on GitHub.
The current Tenant is resolved considering the Issuer field of the JWT token.
Hence the token is verified against the JWKS exposed at the corresponding OpenID Connect well known endpoint.
This is my Security Configuration:
#EnableWebSecurity
#RequiredArgsConstructor
#EnableAutoConfiguration(exclude = UserDetailsServiceAutoConfiguration.class)
public class OrganizationSecurityConfiguration extends WebSecurityConfigurerAdapter {
private final TenantService tenantService;
private List<Tenant> tenants;
#PostConstruct
public void init() {
this.tenants = this.tenantService.findAllWithRelationships();
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests().anyRequest().authenticated()
.and()
.oauth2ResourceServer()
.authenticationManagerResolver(new MultiTenantAuthenticationManagerResolver(this.tenants));
}
}
and this is my custom AuthenticationManagerResolver:
public class MultiTenantAuthenticationManagerResolver implements AuthenticationManagerResolver<HttpServletRequest> {
private final AuthenticationManagerResolver<HttpServletRequest> resolver;
private List<Tenant> tenants;
public MultiTenantAuthenticationManagerResolver(List<Tenant> tenants) {
this.tenants = tenants;
List<String> trustedIssuers = this.tenants.stream()
.map(Tenant::getIssuers)
.flatMap(urls -> urls.stream().map(URL::toString))
.collect(Collectors.toList());
this.resolver = new JwtIssuerAuthenticationManagerResolver(trustedIssuers);
}
#Override
public AuthenticationManager resolve(HttpServletRequest context) {
return this.resolver.resolve(context);
}
}
Now, because of the design of org.springframework.security.oauth2.server.resource.authentication.JwtIssuerAuthenticationManagerResolver.TrustedIssuerJwtAuthenticationManagerResolver
which is private, the only way I can think in order to extract a custom principal is to reimplement everything that follows:
TrustedIssuerJwtAuthenticationManagerResolver
the returned AuthenticationManager
the AuthenticationConverter
the CustomAuthenticationToken which extends JwtAuthenticationToken
the CustomPrincipal
To me it seems a lot of Reinventing the wheel, where my only need would be to have a custom Principal.
The examples that I found don't seem to suit my case since they refer to OAuth2Client or are not tought for Multitenancy.
https://www.baeldung.com/spring-security-oauth-principal-authorities-extractor
How to extend OAuth2 principal
Do I really need to reimplement all such classes/interfaes or is there a smarter approach?
This is how I did it, without reimplementing a huge amount of classes. This is without using a JwtAuthenticationToken however.
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
...
#Override
protected void configure(HttpSecurity http) throws Exception {
http
...
.oauth2ResourceServer(oauth2 -> oauth2.authenticationManagerResolver(authenticationManagerResolver()));
}
#Bean
JwtIssuerAuthenticationManagerResolver authenticationManagerResolver() {
List<String> issuers = ... // get this from list of tennants or config, whatever
Predicate<String> trustedIssuer = issuers::contains;
Map<String, AuthenticationManager> authenticationManagers = new ConcurrentHashMap<>();
AuthenticationManagerResolver<String> resolver = (String issuer) -> {
if (trustedIssuer.test(issuer)) {
return authenticationManagers.computeIfAbsent(issuer, k -> {
var jwtDecoder = JwtDecoders.fromIssuerLocation(issuer);
var provider = new JwtAuthenticationProvider(jwtDecoder);
provider.setJwtAuthenticationConverter(jwtAuthenticationService::loadUserByJwt);
return provider::authenticate;
});
}
return null;
};
return new JwtIssuerAuthenticationManagerResolver(resolver);
}
}
#Service
public class JwtAuthenticationService {
public AbstractAuthenticationToken loadUserByJwt(Jwt jwt) {
UserDetails userDetails = ... // or your choice of principal
List<GrantedAuthority> authorities = ... // extract from jwt or db
...
return new UsernamePasswordAuthenticationToken(userDetails, null, authorities);
}
}

Resources