I have an Spring Boot App (2.1.6) implemented with Kotlin. Is a Rest api that wants to have oAuth 2 with Keycloak.
I have this code in Java that compiles ok:
package com.talleres.paco.mako.config.security;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.autoconfigure.security.oauth2.resource.ResourceServerProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.oauth2.client.OAuth2RestTemplate;
import org.springframework.security.oauth2.client.resource.OAuth2ProtectedResourceDetails;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer;
import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configurers.ResourceServerSecurityConfigurer;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
#Configuration
#EnableWebSecurity
#EnableResourceServer
#EnableGlobalMethodSecurity(prePostEnabled = true)
#ConditionalOnProperty(prefix = "rest.security", value = "enabled", havingValue = "true")
#Import({SecurityProperties.class})
public class SecurityConfigurer extends ResourceServerConfigurerAdapter {
#Autowired
private ResourceServerProperties resourceServerProperties;
#Autowired
private SecurityProperties securityProperties;
#Override
public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
resources.resourceId(resourceServerProperties.getResourceId());
}
#Override
public void configure(final HttpSecurity http) throws Exception {
http.cors()
.configurationSource(corsConfigurationSource())
.and()
.headers()
.frameOptions()
.disable()
.and()
.csrf()
.disable()
.authorizeRequests()
.antMatchers(securityProperties.getApiMatcher())
.authenticated();
}
#Bean
public CorsConfigurationSource corsConfigurationSource() {
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
if (null != securityProperties.getCorsConfiguration()) {
source.registerCorsConfiguration("/**", securityProperties.getCorsConfiguration());
}
return source;
}
#Bean
public JwtAccessTokenCustomizer jwtAccessTokenCustomizer(ObjectMapper mapper) {
return new JwtAccessTokenCustomizer(mapper);
}
#Bean
public OAuth2RestTemplate oauth2RestTemplate(OAuth2ProtectedResourceDetails details) {
OAuth2RestTemplate oAuth2RestTemplate = new OAuth2RestTemplate(details);
//Prepare by getting access token once
oAuth2RestTemplate.getAccessToken();
return oAuth2RestTemplate;
}
}
When i convert to Kotlin i obtain a syntax error:
package com.talleres.paco.mako.config.security
import com.fasterxml.jackson.databind.ObjectMapper
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty
import org.springframework.boot.autoconfigure.security.oauth2.resource.ResourceServerProperties
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.context.annotation.Import
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity
import org.springframework.security.config.annotation.web.builders.HttpSecurity
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity
import org.springframework.security.oauth2.client.OAuth2RestTemplate
import org.springframework.security.oauth2.client.resource.OAuth2ProtectedResourceDetails
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer
import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter
import org.springframework.security.oauth2.config.annotation.web.configurers.ResourceServerSecurityConfigurer
import org.springframework.web.cors.CorsConfigurationSource
import org.springframework.web.cors.UrlBasedCorsConfigurationSource
#Configuration
#EnableWebSecurity
#EnableResourceServer
#EnableGlobalMethodSecurity(prePostEnabled = true)
#ConditionalOnProperty(prefix = "rest.security", value = ["enabled"], havingValue = "true")
#Import({SecurityProperties.class})
class SecurityConfigurer : ResourceServerConfigurerAdapter() {
#Autowired
private val resourceServerProperties: ResourceServerProperties? = null
#Autowired
private val securityProperties: SecurityProperties? = null
#Throws(Exception::class)
override fun configure(resources: ResourceServerSecurityConfigurer) {
resources.resourceId(resourceServerProperties!!.resourceId)
}
#Throws(Exception::class)
override fun configure(http: HttpSecurity) {
http.cors()
.configurationSource(corsConfigurationSource())
.and()
.headers()
.frameOptions()
.disable()
.and()
.csrf()
.disable()
.authorizeRequests()
.antMatchers(securityProperties!!.apiMatcher)
.authenticated()
}
#Bean
fun corsConfigurationSource(): CorsConfigurationSource {
val source = UrlBasedCorsConfigurationSource()
if (securityProperties?.corsConfiguration != null) {
source.registerCorsConfiguration("/**", securityProperties.corsConfiguration);
}
return source
}
#Bean
fun jwtAccessTokenCustomizer(mapper: ObjectMapper): JwtAccessTokenCustomizer {
return JwtAccessTokenCustomizer(mapper)
}
#Bean
fun oauth2RestTemplate(details: OAuth2ProtectedResourceDetails): OAuth2RestTemplate {
val oAuth2RestTemplate = OAuth2RestTemplate(details)
oAuth2RestTemplate.accessToken
return oAuth2RestTemplate
}
}
The error is in the line with the annotation import:
#Import({SecurityProperties.class})
I convert the code from Java to Kotlin with IntelliJ CE. The message is:
> Task :compileKotlin
e: D:\Workspaces\CleanArchitecture\mako\src\main\customized\kotlin\com\talleres\paco\mako\config\security\SecurityConfigurer.kt: (26, 34): Name expected
Thanks in advance.
I guess the tool that converts Java code to Kotlin is not always working. In this case #Import is defined in Java and it expects an array of classes, and you can use it in Kotlin by passing a vararg KClass (actually that works only for annotation's value field, otherwise you need to pass a proper array. More info here).
In other words, you need to change your code to: #Import(SecurityProperties::class).
EDIT: it seems like this issue was reported and fixed years ago: KT-10545. I also tried converting your Java code to Kotlin on my machine (using Kotlin 1.3.41) and the #Import statement was correctly generated.
Something funny, though, happened. The #ConditionalOnProperty line was converted to this:
#ConditionalOnProperty(prefix = "rest.security", value = "enabled", havingValue = "true")
which doesn't compile as "Assigning single elements to varargs in named form is forbidden". I'd be curious to see if it's a regression, as it looks correct in your snippet.
Related
I am not able to create a simple rest API with digest authentication with spring boot 3. It was working with version 2, but with a different configuration, because WebSecurityConfigurerAdapter is not present anymore.
I create simple GitHub repository with the sample:
https://github.com/martinspudich/spring-boot-3-digest
I will appreciate any help you can provide. Thank you in advance.
I create simple HelloController:
package com.example.springboot3digest.controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
#RestController
public class HelloController {
#GetMapping("/")
public String hello() {
return "Hello World!";
}
}
And WebSecurityConfig:
package com.example.springboot3digest.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.password.NoOpPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;
import org.springframework.security.web.authentication.www.DigestAuthenticationEntryPoint;
import org.springframework.security.web.authentication.www.DigestAuthenticationFilter;
#Configuration
#EnableWebSecurity
public class WebSecurityConfig {
#Bean
public SecurityFilterChain filterChain(HttpSecurity http, UserDetailsService userDetailsService) throws Exception {
http
.authorizeHttpRequests(request -> {
request.anyRequest().authenticated();
})
.exceptionHandling(e -> e.authenticationEntryPoint(entryPoint()))
.addFilterBefore(digestAuthenticationFilter(userDetailsService), BasicAuthenticationFilter.class);
return http.build();
}
#Bean
UserDetailsService userDetailsService() {
UserDetails user = User.builder()
.username("user")
.password("user")
.roles("USER").build();
return new InMemoryUserDetailsManager(user);
}
#Bean
public PasswordEncoder passwordEncoder() {
return NoOpPasswordEncoder.getInstance();
}
DigestAuthenticationEntryPoint entryPoint() {
DigestAuthenticationEntryPoint result = new DigestAuthenticationEntryPoint();
result.setRealmName("My App Realm");
result.setKey("3028472b-da34-4501-bfd8-a355c42bdf92");
return result;
}
DigestAuthenticationFilter digestAuthenticationFilter(UserDetailsService userDetailsService) {
DigestAuthenticationFilter result = new DigestAuthenticationFilter();
result.setUserDetailsService(userDetailsService);
result.setAuthenticationEntryPoint(entryPoint());
return result;
}
}
I was following spring.io documentation:
https://docs.spring.io/spring-security/reference/servlet/authentication/passwords/digest.html
But authentication is not working. When I try to authenticate it returns the error 403 Forbidden.
The DigestAuthenticationFilter does not generate "authenticated" tokens unless you explicitly tell it to do so as described here.
Add the line:
result.setCreateAuthenticatedToken(true);
to your DigestAuthenticationFilter instantiation:
DigestAuthenticationFilter digestAuthenticationFilter(UserDetailsService userDetailsService) {
DigestAuthenticationFilter result = new DigestAuthenticationFilter();
result.setUserDetailsService(userDetailsService);
result.setCreateAuthenticatedToken(true);
result.setAuthenticationEntryPoint(entryPoint());
return result;
}
I have MasterSecurity config which is to manage login of masters. And
AdminSecurity config which manages admin login.
When I comment one out the other works. But when I try to use both then the master login shows
PostMapping not allowed
package Spring.LoginRegister.Config;
import Spring.LoginRegister.Entity.RolesConstant;
import Spring.LoginRegister.Repository.AdminRepository;
import Spring.LoginRegister.Service.CustomAdminDetailsService;
import lombok.AllArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
#Configuration
#EnableWebSecurity
#AllArgsConstructor
#Order(1)
public class AdminSecurityConfig {
private final AdminRepository adminRepository;
#Bean
public UserDetailsService userDetailsService1(){
return new CustomAdminDetailsService(adminRepository);
}
#Bean
public BCryptPasswordEncoder passwordEncoder1(){
return new BCryptPasswordEncoder();
}
#Bean
public DaoAuthenticationProvider authenticationProvider1(){
DaoAuthenticationProvider authProvider= new DaoAuthenticationProvider();
authProvider.setUserDetailsService(userDetailsService1());
authProvider.setPasswordEncoder(passwordEncoder1());
return authProvider;
}
#Bean
public SecurityFilterChain AdminsecurityFilterChain(HttpSecurity http) throws Exception {
http
.authenticationProvider(authenticationProvider1());
http
.csrf().disable()
.authorizeHttpRequests((request) -> request
.requestMatchers("/AdminDashBoard/**").authenticated()
.requestMatchers("/admin/login").hasRole(RolesConstant.ROLE_ADMIN.toString() )
.anyRequest().permitAll()
)
.formLogin((form) ->form
.loginPage("/admin/login")
.defaultSuccessUrl("/AdminDashBoard", true)
.permitAll()
)
.logout(form -> form
.logoutUrl("/logout")
.invalidateHttpSession(true)
.clearAuthentication(true)
.logoutSuccessUrl("/")
);
return http.build();
}
}
My problem is they both work individually when one is removed from the app. When I try to combine both only the admin config works properly. This error comes
Method 'POST' is not supported.
org.springframework.web.HttpRequestMethodNotSupportedException: Request method 'POST' is not supported
at org.springframework.web.servlet.mvc.method.RequestMappingInfoHandlerMapping.handleNoMatch(RequestMappingInfoHandlerMapping.java:265)
at org.springframework.web.servlet.handler.AbstractHandlerMethodMapping.lookupHandlerMethod(AbstractHandlerMethodMapping.java:441)
at org.springframework.web.servlet.handler.AbstractHandlerMethodMapping.getHandlerInternal(AbstractHandlerMethodMapping.java:382)
at org.springframework.web.servlet.mvc.method.RequestMappingInfoHandlerMapping.getHandlerInternal(RequestMappingInfoHandlerMapping.java:126)
at org.springframework.web.servlet.mvc.method.RequestMappingInfoHandlerMapping.getHandlerInternal(RequestMappingInfoHandlerMapping.java:68)
at org.springframework.web.servlet.handler.AbstractHandlerMapping.getHandler(AbstractHandlerMapping.java:504)
at org.springframework.web.servlet.DispatcherServlet.getHandler(DispatcherServlet.java:1274)
at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1056)
This is my second config:
package Spring.LoginRegister.Config;
import Spring.LoginRegister.Repository.MasterRepository;
import Spring.LoginRegister.Service.CustomAdminDetailsService;
import Spring.LoginRegister.Service.CustomMasterDetails;
import Spring.LoginRegister.Service.CustomMasterDetailsService;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.core.annotation.Order;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
#Configuration
#Order(2)
public class MASTERSecurityConfig {
private MasterRepository masterRepository;
#Bean
public UserDetailsService userDetailsService2(){
return new CustomMasterDetailsService(masterRepository);
}
#Bean
#Primary
public BCryptPasswordEncoder passwordEncoder2(){
return new BCryptPasswordEncoder();
}
#Bean
public DaoAuthenticationProvider authenticationProvider2(){
DaoAuthenticationProvider authProvider2= new DaoAuthenticationProvider();
authProvider2.setUserDetailsService(userDetailsService2());
authProvider2.setPasswordEncoder(passwordEncoder2());
return authProvider2;
}
#Bean
public SecurityFilterChain MastersecurityFilterChain(HttpSecurity http) throws Exception {
http
.authenticationProvider(authenticationProvider2());
http
.csrf().disable()
.authorizeHttpRequests((request) ->request
.requestMatchers("/master/home/**").authenticated()
.requestMatchers("/master/login")
.hasRole(RolesConstant.ROLE_HOUSEMASTER.toString())
.anyRequest().permitAll()
)
.formLogin((form) ->form
.loginPage("/master/login")
.defaultSuccessUrl("/master/home", true)
.permitAll())
.logout(form -> form
.logoutUrl("/logout")
.invalidateHttpSession(true)
.logoutSuccessUrl("/")
.permitAll()
);
return http.build();
}
}
I tried StackOverflow previous answers.
Do not make separate Config classes. Just make multiple security filter chains and order them accordingly. Also, you have to prevent creating Beans of relative components as beans are global and they will contradict with each other.
I am using new spring authorization server. And everything was working fine with version 0.3.1. But after migration to 1.0.0. When I am trying to login (and I am supposed to be redirected to /authorized?code=) I am always getting redirected back to login. Could somebody please explain me what is going wrong? Maybe config for security should be specified in one place only?
Thank you.
AuthorizationServerConfig
package oauth2.config;
import com.nimbusds.jose.jwk.JWKSet;
import com.nimbusds.jose.jwk.RSAKey;
import com.nimbusds.jose.jwk.source.JWKSource;
import com.nimbusds.jose.proc.SecurityContext;
import oauth2.converter.AuthorizationClientConverter;
import oauth2.handler.FederatedIdentityAuthenticationSuccessHandler;
import oauth2.repository.AuthorizationClientRepository;
import oauth2.repository.ClientRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configurers.oauth2.server.resource.OAuth2ResourceServerConfigurer;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.oauth2.core.AuthorizationGrantType;
import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
import org.springframework.security.oauth2.jwt.JwtClaimsSet;
import org.springframework.security.oauth2.server.authorization.OAuth2TokenType;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository;
import org.springframework.security.oauth2.server.authorization.config.annotation.web.configuration.OAuth2AuthorizationServerConfiguration;
import org.springframework.security.oauth2.server.authorization.config.annotation.web.configurers.OAuth2AuthorizationServerConfigurer;
import org.springframework.security.oauth2.server.authorization.settings.AuthorizationServerSettings;
import org.springframework.security.oauth2.server.authorization.settings.ClientSettings;
import org.springframework.security.oauth2.server.authorization.settings.TokenSettings;
import org.springframework.security.oauth2.server.authorization.token.JwtEncodingContext;
import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenCustomizer;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.util.matcher.RequestMatcher;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.interfaces.RSAPrivateKey;
import java.security.interfaces.RSAPublicKey;
import java.time.Duration;
import java.util.UUID;
#Configuration(proxyBeanMethods = false)
public class AuthorizationServerConfig {
#Autowired
private PasswordEncoder passwordEncoder;
#Autowired
private AuthorizationClientConverter authorizationClientConverter;
#Autowired
private FederatedIdentityAuthenticationSuccessHandler federatedIdentityAuthenticationSuccessHandler;
#Bean
#Order(Ordered.HIGHEST_PRECEDENCE)
public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception {
OAuth2AuthorizationServerConfigurer authorizationServerConfigurer =
new OAuth2AuthorizationServerConfigurer();
RequestMatcher endpointsMatcher = authorizationServerConfigurer.getEndpointsMatcher();
http
.securityMatcher(endpointsMatcher)
.csrf(csrf -> csrf.ignoringRequestMatchers(endpointsMatcher))
.authorizeHttpRequests(auth -> {
auth.anyRequest().authenticated();
})
.formLogin(Customizer.withDefaults())
.oauth2Login(Customizer.withDefaults())
.oauth2ResourceServer(OAuth2ResourceServerConfigurer::jwt)
.apply(authorizationServerConfigurer);
return http.build();
}
#Bean
public OAuth2TokenCustomizer<JwtEncodingContext> jwtCustomizer() {
return context -> {
JwtClaimsSet.Builder claims = context.getClaims();
if (context.getTokenType().equals(OAuth2TokenType.ACCESS_TOKEN)) {
claims.claim("claim", "zabur_claim");
}
};
}
#Bean
public RegisteredClientRepository registeredClientRepository(ClientRepository clientRepository) {
RegisteredClient registeredClient = RegisteredClient.withId("messaging-client")
.clientId("messaging-client")
.clientSecret(passwordEncoder.encode("secret"))
.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
.authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN)
.authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS)
.redirectUri("http://127.0.0.1:8080/authorized")
.scope("message.read")
.scope("message.write")
.clientSettings(ClientSettings.builder().requireAuthorizationConsent(true).build())
.build();
AuthorizationClientRepository registeredClientRepository = new AuthorizationClientRepository(authorizationClientConverter, clientRepository);
registeredClientRepository.save(registeredClient);
return registeredClientRepository;
}
#Bean
public JWKSource<SecurityContext> jwkSource() {
RSAKey rsaKey = generateRsa();
JWKSet jwkSet = new JWKSet(rsaKey);
return (jwkSelector, securityContext) -> jwkSelector.select(jwkSet);
}
#Bean
public AuthorizationServerSettings authorizationServerSettings() {
return AuthorizationServerSettings.builder()
.issuer("http://localhost:8080")
.build();
}
#Bean
public TokenSettings tokenSettings() {
return TokenSettings.builder()
.accessTokenTimeToLive(Duration.ofMinutes(5))
.build();
}
private RSAKey generateRsa() {
KeyPair keyPair = generateRsaKey();
RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic();
RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate();
// #formatter:off
return new RSAKey.Builder(publicKey)
.privateKey(privateKey)
.keyID(UUID.randomUUID().toString())
.build();
// #formatter:on
}
private KeyPair generateRsaKey() {
KeyPair keyPair;
try {
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
keyPairGenerator.initialize(2048);
keyPair = keyPairGenerator.generateKeyPair();
} catch (Exception ex) {
throw new IllegalStateException(ex);
}
return keyPair;
}
}
SecurityConfig
package oauth2.config;
import oauth2.handler.FederatedIdentityAuthenticationSuccessHandler;
import oauth2.handler.FormLoginAuthenticationSuccessHandler;
import oauth2.service.impl.CustomUserDetailsService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
#Configuration
#EnableWebSecurity
public class SecurityConfig {
#Autowired
private PasswordEncoder passwordEncoder;
#Autowired
private FederatedIdentityAuthenticationSuccessHandler federatedIdentityAuthenticationSuccessHandler;
#Autowired
private FormLoginAuthenticationSuccessHandler formLoginAuthenticationSuccessHandler;
#Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.csrf(csrf -> csrf.ignoringRequestMatchers("/signUp", "/logout"))
.authorizeHttpRequests(auth -> {
auth.requestMatchers("/logout", "/signUp", "/authorized").permitAll();
auth.anyRequest().authenticated();
})
.formLogin(formLogin -> formLogin.successHandler(formLoginAuthenticationSuccessHandler))
.oauth2Login(oauth2Login -> oauth2Login.successHandler(federatedIdentityAuthenticationSuccessHandler));
return http.build();
}
#Bean
public DaoAuthenticationProvider authenticationProvider(CustomUserDetailsService customUserDetailsService) {
DaoAuthenticationProvider authenticationProvider = new DaoAuthenticationProvider();
authenticationProvider.setPasswordEncoder(passwordEncoder);
authenticationProvider.setUserDetailsService(customUserDetailsService);
return authenticationProvider;
}
}
I am trying to login with OAuth2 and expecting to be redirected to the /authorized?code= but after that I am redirected to /login.
Might you are using lot more code to serve your purpose. I integrated OAuth 2 in my Application using Spring security 6.0.0 and Spring Authorization Server 1.0.0. It made me sweating lot. But I got success. For that I shared my Authorization server configuration as it did.
#Bean
#Order(Ordered.HIGHEST_PRECEDENCE)
public SecurityFilterChain authServerSecurityFilterChain(HttpSecurity http) throws Exception {
OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http);
http
.getConfigurer(OAuth2AuthorizationServerConfigurer.class)
.oidc(Customizer.withDefaults()); // Enable OpenID Connect 1.0
http
.exceptionHandling((exceptions) -> exceptions
.authenticationEntryPoint(
new LoginUrlAuthenticationEntryPoint("/login"))
)
.oauth2ResourceServer(OAuth2ResourceServerConfigurer::jwt);
return http.build();
}
#Bean
#Order(Ordered.HIGHEST_PRECEDENCE + 1)
public SecurityFilterChain securityFilterChain(HttpSecurity http)
throws Exception {
http
.cors().and().csrf().disable()
.headers().frameOptions().sameOrigin()
.httpStrictTransportSecurity().disable()
.and()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)
.and()
.authorizeHttpRequests((authorize) -> authorize
.anyRequest().authenticated()
)
.formLogin(Customizer.withDefaults());
return http.build();
}
I think it may help you a little bit or you may get an idea what actually going on in your code.
SecurityFilterChain by default masks any request for authentication, it acts as a safety net, shadowing any permitAll() matcher. This is somehow not clear in the documentation, as per my understanding for the documentation, and needs to be better clarified.
Allowable paths must be declared in a SecurityFilterChain Bean, while authenticated and authorized paths must be kept in another SecurityChain Bean. To differentiate between allowed and authorized paths, use '.securityMaters()', as shown in the example below:
#Bean
#Order(0)
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.csrf(csrf -> csrf.ignoringRequestMatchers("/signUp", "/logout"))
.securityMatcher("/logout", "/signUp", "/authorized")
.authorizeHttpRequests(auth -> {
auth.anyRequest().permitAll();
});
return http.build();
}
#Bean
#Order(1)
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.csrf(csrf -> csrf.ignoringRequestMatchers("/signUp", "/logout"))
.authorizeHttpRequests(auth -> {
auth.anyRequest().authenticated();
})
.formLogin(formLogin -> formLogin.successHandler(formLoginAuthenticationSuccessHandler))
.oauth2Login(oauth2Login -> oauth2Login.successHandler(federatedIdentityAuthenticationSuccessHandler));
return http.build();
}
Try it and let me know how it goes with you!
I have a Keycloak instance and created two realms and one user for each realm.
Realm1 (Tenant1) -> User 1
Realm2 (Tenant2) -> User 2
And i have my spring boot application.yml (resource server - API) for one specific realm and fixed in my code.
keycloak:
realm: Realm1
auth-server-url: https://localhost:8443/auth
ssl-required: external
resource: app
bearer-only: true
use-resource-role-mappings: true
It's working and validate for Realm1.
but now i can receive requests from user2 (tenant2) and the token will not be valid because the public key (realm1) is not valid for the signed request jwt token (realm2).
What is the best way to allow multi tenancy and dynamically configuration for multi realms?
thanks,
There's a whole chapter on it: 2.1.18: Multi-Tenanacy
Instead of defining the keycloak config in spring application.yaml, keep multiple keycloak.json config files, and use a custom KeycloakConfigResolver:
public class PathBasedKeycloakConfigResolver implements KeycloakConfigResolver {
#Override
public KeycloakDeployment resolve(OIDCHttpFacade.Request request) {
if (request.getPath().startsWith("alternative")) { // or some other criteria
InputStream is = getClass().getResourceAsStream("/tenant1-keycloak.json");
return KeycloakDeploymentBuilder.build(is); //TODO: cache result
} else {
InputStream is = getClass().getResourceAsStream("/default-keycloak.json");
return KeycloakDeploymentBuilder.build(is); //TODO: cache result
}
}
}
I'm not sure if this works well with the keycloak-spring-boot-starter, but I think it's enough to just wire your custom KeycloakConfigResolver in the KeycloakWebSecurityConfigurerAdapter:
#Configuration
#EnableWebSecurity
class SecurityConfig extends KeycloakWebSecurityConfigurerAdapter {
#Bean
public KeycloakConfigResolver keycloakConfigResolver() {
return new PathBasedKeycloakConfigResolver();
}
[...]
}
import org.keycloak.adapters.springsecurity.KeycloakConfiguration;
import org.keycloak.adapters.springsecurity.authentication.KeycloakAuthenticationProvider;
import org.keycloak.adapters.springsecurity.config.KeycloakWebSecurityConfigurerAdapter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.DependsOn;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.core.authority.mapping.SimpleAuthorityMapper;
import org.springframework.security.core.session.SessionRegistryImpl;
import org.springframework.security.web.authentication.session.RegisterSessionAuthenticationStrategy;
import org.springframework.security.web.authentication.session.SessionAuthenticationStrategy;
#DependsOn("keycloakConfigResolver")
#KeycloakConfiguration
#EnableGlobalMethodSecurity(jsr250Enabled = true)
#ConditionalOnProperty(name = "keycloak.enabled", havingValue = "true", matchIfMissing = true)
public class SecurityConfig extends KeycloakWebSecurityConfigurerAdapter {
/**
* Registers the KeycloakAuthenticationProvider with the authentication manager.
*/
#Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
KeycloakAuthenticationProvider authenticationProvider = new KeycloakAuthenticationProvider();
authenticationProvider.setGrantedAuthoritiesMapper(new SimpleAuthorityMapper());
auth.authenticationProvider(authenticationProvider);
}
/**
* Defines the session authentication strategy.
*/
#Bean
#Override
protected SessionAuthenticationStrategy sessionAuthenticationStrategy() {
return new RegisterSessionAuthenticationStrategy(new SessionRegistryImpl());
}
#Override
protected void configure(HttpSecurity http) throws Exception {
super.configure(http);
http
.cors()
.and()
.authorizeRequests().antMatchers(HttpMethod.OPTIONS)
.permitAll()
.antMatchers("/api-docs/**", "/configuration/ui",
"/swagger-resources/**", "/configuration/**", "/v2/api-docs",
"/swagger-ui.html/**", "/webjars/**", "/swagger-ui/**")
.permitAll()
.anyRequest().authenticated();
}
}
import org.keycloak.adapters.KeycloakConfigResolver;
import org.keycloak.adapters.KeycloakDeployment;
import org.keycloak.adapters.KeycloakDeploymentBuilder;
import org.keycloak.adapters.OIDCHttpFacade;
import java.io.InputStream;
import java.util.concurrent.ConcurrentHashMap;
public class PathBasedConfigResolver implements KeycloakConfigResolver {
private final ConcurrentHashMap<String, KeycloakDeployment> cache = new ConcurrentHashMap<>();
#Override
public KeycloakDeployment resolve(OIDCHttpFacade.Request request) {
String path = request.getURI();
String realm = "realmName";
if (!cache.containsKey(realm)) {
InputStream is = getClass().getResourceAsStream("/" + realm + "-keycloak.json");
cache.put(realm, KeycloakDeploymentBuilder.build(is));
}
return cache.get(realm);
}
}
import org.keycloak.adapters.KeycloakConfigResolver;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.mongo.MongoAutoConfiguration;
import org.springframework.context.annotation.Bean;
#SpringBootApplication()
public class Application {
public static void main(String[] args) {
SpringApplication.run(DIVMasterApplication.class, args);
}
#Bean
#ConditionalOnMissingBean(PathBasedConfigResolver.class)
public KeycloakConfigResolver keycloakConfigResolver() {
return new PathBasedConfigResolver();
}
}
I wanted to pass in JSON instead of using params while logging in. So what I do is I create a filter, however, the strange thing is that the filter itself doesn't get invoke at all (Or basically when I try logging in, the request by pass it, completely ignore my filter). The request go straight to my AuthenticationHandler. I have gone through many posts and I still have no clue of why would this happen, especially when I replicate the same structure of code in Java but it works perfectly as intended...
Did I miss something obvious? Here's the UsernamePasswordAuthenticationFilter and my security config. My Java version works fine, but my Kotlin version completely ignores the filter.
It doesn't return 404 as well, it returns my AuthenticationFailureHandler.
import com.fasterxml.jackson.annotation.JsonCreator
import com.fasterxml.jackson.annotation.JsonProperty
import com.fasterxml.jackson.databind.ObjectMapper
import lombok.Getter
import org.apache.commons.io.IOUtils
import org.springframework.http.HttpMethod
import org.springframework.security.authentication.AuthenticationServiceException
import org.springframework.security.authentication.InternalAuthenticationServiceException
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken
import org.springframework.security.core.Authentication
import org.springframework.security.core.AuthenticationException
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter
import javax.servlet.http.HttpServletRequest
import javax.servlet.http.HttpServletResponse
import java.io.IOException
import java.nio.charset.Charset
class JsonLoginFilter : UsernamePasswordAuthenticationFilter() {
#Throws(AuthenticationException::class)
override fun attemptAuthentication(request: HttpServletRequest, response: HttpServletResponse?): Authentication {
if (!HttpMethod.POST.matches(request.method)) {
throw AuthenticationServiceException("Authentication method not supported: " + request.method)
}
val payload: String
try {
payload = IOUtils.toString(request.inputStream, Charset.defaultCharset())
val auth = ObjectMapper().readValue(payload, JsonAuthenticationParser::class.java)
// println(auth.username)
// println(auth.password)
val authRequest = UsernamePasswordAuthenticationToken(auth.username, auth.password)
return this.authenticationManager.authenticate(authRequest)
} catch (e: IOException) {
throw InternalAuthenticationServiceException("Could not parse authentication payload")
}
}
#Getter
data class JsonAuthenticationParser #JsonCreator
constructor(#param:JsonProperty("username")
val username: String,
#param:JsonProperty("password")
val password: String)
}
My Security config in Kotlin
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.context.annotation.Bean
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder
import org.springframework.security.config.annotation.web.builders.HttpSecurity
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter
import org.springframework.security.web.authentication.logout.HttpStatusReturningLogoutSuccessHandler
#EnableWebSecurity
class WebSecurity: WebSecurityConfigurerAdapter() {
#Autowired
private lateinit var entryConfig: EntryConfig
#Autowired
private lateinit var failAuth: FailAuthentication
#Autowired
private lateinit var successAuthentication: SuccessAuthentication
#Autowired
private lateinit var authenticationHandler: AuthenticationHandler
#Throws(Exception::class)
override fun configure(http: HttpSecurity) {
http
.authorizeRequests()
.antMatchers("/api/v1/traveller/add","/api/v1/symptoms","/api/v1/flights","/api/v1/user/login","/api/v1/user/logout").permitAll()
.antMatchers("/api/v1/user/**","/api/v1/traveller/**").hasRole("ADMIN")
.antMatchers("/**").authenticated()
.and()
.addFilterAt(authenFilter(), UsernamePasswordAuthenticationFilter::class.java)
.formLogin().loginProcessingUrl("/api/v1/user/login")
.successHandler(successAuthentication).failureHandler(failAuth)
.and()
.exceptionHandling().authenticationEntryPoint(entryConfig)
.and()
.cors()
.and()
.logout().logoutUrl("/api/v1/user/logout")
.clearAuthentication(true)
.invalidateHttpSession(true)
.deleteCookies("JSESSIONID")
.logoutSuccessHandler(HttpStatusReturningLogoutSuccessHandler())
.permitAll()
//
http
.csrf()
.disable()
}
#Throws(Exception::class)
override fun configure(auth: AuthenticationManagerBuilder) {
auth.authenticationProvider(authenticationHandler)
}
#Bean
#Throws(Exception::class)
fun authenFilter(): JsonLoginFilter {
var filter : JsonLoginFilter = JsonLoginFilter()
filter.setAuthenticationManager(authenticationManagerBean())
filter.setAuthenticationSuccessHandler(successAuthentication)
filter.setAuthenticationFailureHandler(failAuth)
return filter
}
#Bean
fun passwordEncoder(): BCryptPasswordEncoder {
return BCryptPasswordEncoder()
}
}
My Java version, slightly differ but I believe it should have the same structure
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.Getter;
import org.apache.commons.io.IOUtils;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationServiceException;
import org.springframework.security.authentication.InternalAuthenticationServiceException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.nio.charset.Charset;
public class JsonAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
#Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
System.out.println("hello");
if (! HttpMethod.POST.matches(request.getMethod())) {
throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
}
String payload;
try {
payload = IOUtils.toString(request.getInputStream(), Charset.defaultCharset());
JsonAuthenticationParser auth = new ObjectMapper().readValue(payload, JsonAuthenticationParser.class);
System.out.println(auth.username);
System.out.println(auth.password);
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(auth.username, auth.password);
return this.getAuthenticationManager().authenticate(authRequest);
} catch (IOException e) {
throw new InternalAuthenticationServiceException("Could not parse authentication payload");
}
}
#Getter
static class JsonAuthenticationParser {
private final String username;
private final String password;
#JsonCreator
public JsonAuthenticationParser(#JsonProperty("username") String username, #JsonProperty("password") String password) {
this.username = username;
this.password = password;
}
}
}
Security config in Java
import hard.string.security.AuthenticationHandler;
import hard.string.security.EntryConfig;
import hard.string.security.FailAuthhentication;
import hard.string.security.SuccessAuthentication;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.authentication.logout.HttpStatusReturningLogoutSuccessHandler;
#EnableWebSecurity
public class WebSecurity extends WebSecurityConfigurerAdapter {
#Autowired
private EntryConfig entryConfig;
#Autowired
private FailAuthhentication failAuth;
#Autowired
private SuccessAuthentication successAuthentication;
#Autowired
private AuthenticationHandler authenticationHandler;
#Bean
public JsonAuthenticationFilter authenticationFilter() throws Exception {
JsonAuthenticationFilter filter = new JsonAuthenticationFilter();
filter.setAuthenticationManager(authenticationManagerBean());
// filter.setContinueChainBeforeSuccessfulAuthentication(true);
filter.setAuthenticationSuccessHandler(successAuthentication);
filter.setAuthenticationFailureHandler(failAuth);
return filter;
}
#Override
protected void configure(HttpSecurity http) throws Exception {
// http://stackoverflow.com/questions/19500332/spring-security-and-json-authentication
http
.authorizeRequests()
.antMatchers("/login", "/logout", "/register",
"/debug/**").permitAll()
.antMatchers("/**").authenticated()
.and()
.addFilterAt(authenticationFilter(), UsernamePasswordAuthenticationFilter.class)
.formLogin().loginProcessingUrl("/login")
.successHandler(successAuthentication).failureHandler(failAuth)
.and()
.exceptionHandling().authenticationEntryPoint(entryConfig)
.and()
.cors()
.and()
.logout().logoutUrl("/logout")
.clearAuthentication(true)
.invalidateHttpSession(true)
.deleteCookies("JSESSIONID")
.logoutSuccessHandler(new HttpStatusReturningLogoutSuccessHandler())
.permitAll();
//
http
.csrf()
.disable();
}
#Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.authenticationProvider(authenticationHandler);
}
#Bean
public BCryptPasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
}
Thanks for the help
Ok, After spending days to find the bug. I found out that the filter doesn't automatically link with loginProcessingUrl. You need to specify what url you want to do the filter on or else it will just apply the filter only to localhost:xxxx/login
I just going to leave this question up here just in case someone run into this stupid problem like myself.
fun authenFilter(): JsonLoginFilter {
var filter : JsonLoginFilter = JsonLoginFilter()
filter.setAuthenticationManager(authenticationManagerBean())
filter.setAuthenticationSuccessHandler(successAuthentication)
filter.setAuthenticationFailureHandler(failAuth)
filter.setFilterProcessesUrl("/api/v1/user/login") //HERE
return filter
}