I am building rest API using Spring Boot v1.3.3. API is secured by Spring Security. I have implemented custom user details service to have custom principal in authentication context.
I needed to share sessions of API with other Spring app so I choosen to implement Spring Session with Redis server in my app using this tutorial docs.spring.io/spring-session/docs/current/reference/html5/guides/security.html. Unfortunetly it caused Authentication Principal to stop working. When I am trying to get current Principal either by annotation #AuthenticationPrincipal CustomUserDetails user or by SecurityContextHolder.getContext().getAuthentication().getPrincipal() it returns my custom user details but with Id = 0 and all fields set to null
(screen from debugging). I can't even get username from SecurityContextHolder.getContext().getAuthentication().getName().
After I commented Redis code and maven dependency it works (see debug screen). How to make it working with Spring Session and Redis server?
Here is some code from the app:
Some example method to check Principal
#RequestMapping(value = "/status", method = RequestMethod.GET)
public StatusData status(#AuthenticationPrincipal CustomUserDetails user) {
User user2 = (CustomUserDetails) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
if (user != null) {
String name = user.getUsername();
return new StatusData(name);
} else return new StatusData(null);
}
Application and Redis config:
#Configuration
#EnableRedisHttpSession
public class AppConfig {
#Bean
public JedisConnectionFactory connectionFactory() {
return new JedisConnectionFactory();
}
#Bean
public CookieSerializer cookieSerializer() {
DefaultCookieSerializer serializer = new DefaultCookieSerializer();
serializer.setCookieName("JSESSIONID");
serializer.setCookiePath("/");
serializer.setDomainNamePattern("^.+?\\.(\\w+\\.[a-z]+)$");
return serializer;
}
#Bean
public ShaPasswordEncoder shaEncoder() {
return new ShaPasswordEncoder(256);
}
#Bean
public BCryptPasswordEncoder bCryptPasswordEncoder() {
return new BCryptPasswordEncoder();
}
#Bean(name = "messageSource")
public ResourceBundleMessageSource messageSource() {
ResourceBundleMessageSource resourceBundleMessageSource = new ResourceBundleMessageSource();
resourceBundleMessageSource.setBasename("messages/messages");
return resourceBundleMessageSource;
}
#Bean
public Validator basicValidator() {
LocalValidatorFactoryBean validator = new LocalValidatorFactoryBean();
validator.setValidationMessageSource(messageSource());
return validator;
}
public AppConfig() {
DateTimeZone.setDefault(DateTimeZone.UTC);
}
}
Initializer (used for Redis Session)
public class Initializer extends AbstractHttpSessionApplicationInitializer {
}
SecurityInitializer (used for Redis session)
public class SecurityInitializer extends AbstractSecurityWebApplicationInitializer {
public SecurityInitializer() {
super(WebSecurityConfig.class, AppConfig.class);
}
}
WebSecurityConfig (Spring Security config)
#Configuration
#EnableWebSecurity
//#EnableWebMvcSecurity
#ComponentScan(basePackageClasses = {UserRepository.class, CustomUserDetailsService.class})
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
#Autowired
private DataSource dataSource;
#Autowired
private UserDetailsService customUserDetailsService;
#Autowired
private HttpAuthenticationEntryPoint httpAuthenticationEntryPoint;
#Autowired
private AuthSuccessHandler authSuccessHandler;
#Autowired
private AuthFailureHandler authFailureHandler;
#Autowired
private HttpLogoutSuccessHandler logoutSuccessHandler;
#Autowired
private BCryptPasswordEncoder bCryptPasswordEncoder;
/**
* Persistent token repository stored in database. Used for remember me feature.
*/
#Bean
public PersistentTokenRepository tokenRepository() {
JdbcTokenRepositoryImpl db = new JdbcTokenRepositoryImpl();
db.setDataSource(dataSource);
return db;
}
/**
* Enable always remember feature.
*/
#Bean
public AbstractRememberMeServices rememberMeServices() {
CustomTokenPersistentRememberMeServices rememberMeServices = new CustomTokenPersistentRememberMeServices("xxx", customUserDetailsService, tokenRepository());
rememberMeServices.setAlwaysRemember(true);
rememberMeServices.setTokenValiditySeconds(1209600);
return rememberMeServices;
}
/**
* Configure spring security to use in REST API.
* Set handlers to immediately return HTTP status codes.
* Enable remember me tokens.
*/
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf().disable()
.exceptionHandling()
.authenticationEntryPoint(httpAuthenticationEntryPoint)
.and()
.authorizeRequests()
.antMatchers("/cookie", "/register", "/redirect/**", "/track/**")
.permitAll()
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/login")
.permitAll()
.successHandler(authSuccessHandler)
.failureHandler(authFailureHandler)
.and()
.logout()
.permitAll().logoutUrl("/logout").logoutSuccessHandler(logoutSuccessHandler)
.and()
.rememberMe().rememberMeServices(rememberMeServices())
.and()
.headers()
.addHeaderWriter(new HeaderWriter() {
/**
* Header to allow access from javascript AJAX in chrome extension.
*/
#Override
public void writeHeaders(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse) {
String corsUrl = "https://mail.google.com";
if (httpServletRequest.getHeader("Origin") != null && httpServletRequest.getHeader("Origin").equals(corsUrl)) {
httpServletResponse.setHeader("Access-Control-Allow-Origin", "https://mail.google.com");
httpServletResponse.setHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS");
httpServletResponse.setHeader("Access-Control-Allow-Credentials", "true");
httpServletResponse.setHeader("Access-Control-Expose-Headers", "Location");
}
}
});
}
/**
* Set custom user details service to allow for store custom user details and set password encoder to BCrypt.
*/
#Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
auth
.userDetailsService(customUserDetailsService).passwordEncoder(bCryptPasswordEncoder);
}
}
Maven dependencies
<dependencies>
<dependency>
<groupId>${project.groupId}</groupId>
<artifactId>models</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-validator</artifactId>
<version>5.2.3.Final</version>
</dependency>
<dependency>
<groupId>joda-time</groupId>
<artifactId>joda-time</artifactId>
</dependency>
<dependency>
<groupId>org.jadira.usertype</groupId>
<artifactId>usertype.core</artifactId>
<version>3.1.0.CR1</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.jaxrs</groupId>
<artifactId>jackson-jaxrs-json-provider</artifactId>
<version>2.2.1</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-joda</artifactId>
</dependency>
<dependency>
<groupId>com.maxmind.geoip2</groupId>
<artifactId>geoip2</artifactId>
<version>2.6.0</version>
</dependency>
<dependency>
<groupId>com.ganyo</groupId>
<artifactId>gcm-server</artifactId>
<version>1.0.2</version>
</dependency>
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session</artifactId>
<version>1.1.1.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-redis</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<version>4.0.4.RELEASE</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.jayway.jsonpath</groupId>
<artifactId>json-path</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
I solved this problem. It turned out that Spring-Session serializes the Principal object. My custom implementation of UserDetails was subclass of Hibernate Model User class. I solved it by implementing Serializable interface in my custom UserDetails, User model and all classes used in this model.
To make it work in my case I had as well to make sure the Servlet filters were set up in the right order.
For me that was:
...
<filter-name>CharacterEncodingFilter</filter-name>
...
<filter-name>springSessionRepositoryFilter</filter-name>
...
<filter-name>springSecurityFilterChain</filter-name>
...
<filter-name>csrfFilter</filter-name>
...
After that, the principal was not empty anymore.
As #yglodt said, the problem is the filter's order in the spring security filter chain.
In Java Config way, just set an higher precedence to Redis configuration class
#Configuration
#EnableRedisHttpSession
#Order(Ordered.HIGHEST_PRECEDENCE)
public class RedisConfig extends AbstractHttpSessionApplicationInitializer {
#Bean
public JedisConnectionFactory connectionFactory() {
return new JedisConnectionFactory();
}
}
I set the highest precedence, but maybe something lower is enough.
Now the principal should be correctly populated.
The order of the HttpSecurity chain is important:
Does not work, and leaves principal name null:
.authorizeRequests()
.antMatchers("/api/register").permitAll()
.anyRequest().authenticated()
Works correct:
.authorizeRequests()
.anyRequest().authenticated()
.antMatchers("/api/register").permitAll()
EDIT: 2022 This answer is outdated and will throw an IllegalStateException according to #BendaThierry.com
Related
I need help finding a way to make Spring Security's form login endpoint work in swagger-ui with springdoc-openapi. I'm using SpringBoot 2.7.5 and Spring Security 5.7.4, and these are the rest of the project's dependencies:
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-ui</artifactId>
<version>1.6.12</version>
</dependency>
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-security</artifactId>
<version>1.6.12</version>
</dependency>
</dependencies>
Spring Security simple configuration for securing /foos/ endpoints with form login and defining user login details.
#Configuration
#EnableWebSecurity
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable().authorizeRequests()
.antMatchers("/foos/**")
.authenticated()
.and()
.formLogin()
.permitAll()
.and()
.logout()
.permitAll();
}
#Autowired
public void configureGlobal(AuthenticationManagerBuilder auth, PasswordEncoder passwordEncoder) throws Exception {
auth.inMemoryAuthentication()
.withUser("user")
.password(passwordEncoder.encode("password"))
.roles("USER");
}
}
And the foos Controller:
#RestController
#RequestMapping("foos")
public class FooController {
#GetMapping(value = "/{id}")
public Foo findById(#PathVariable("id") final Long id) {
return new Foo(randomAlphabetic(6));
}
#GetMapping
public List<Foo> findAll() {
return Lists.newArrayList(new Foo(randomAlphabetic(6)));
}
#PostMapping
#ResponseStatus(HttpStatus.CREATED)
public Foo create(#RequestBody final Foo foo) {
return foo;
}
}
Also supplying this property: springdoc.show-login-endpoint=true and the login endpoint is exposed in swagger-ui but the only problem is that the only request body type is application/json and this sends the credentials as json in the request body which results in null username/password in UsernamePasswordAuthenticationFilter.
Sample code repo: https://github.com/adrianbob/springdoc-form-login
Can the request body type be configured to application/x-www-form-urlencoded, so that form login works?
I've raised an issue which was categorised as a bug and fixed. The latest release does not have this problem anymore, version 1.6.13.
when iam adding the Spring Boot Starter Security dependency on my Projekt my STOMP Endpoint responding an 404 Code to my React frontend. I build a simple Demo Projekt with only web socket dependency. In this case everything works fine. When iam adding the security dependency without any configuration i get a 403. At this point everything is fine. When iam adding the same WebSecurityConfigurerAdapter implementation as the Main Projekt everything works fine aswell. But on my main Projekt it did not work. Everytime i get a 404 on my endpoint ws://localhost:8080/socket
I tried to get this work for one Week now... I cant figure it out where i should configure the Security part for the Sockets
The goul of all this is to stream progress information of some Tasks to the frontend. If you have any other solutions to build that i would be happy. It could be that websockets are not the best way to do that.
and btw. its my first Question on Stackoverflow please dont judge me if the formatting is not the best way :-)
Iam storing the User Informations in a h2 Database.
Here my Configurations and Dependencys for the Backend
pom.xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-log4j2 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-log4j2</artifactId>
<version>2.6.6</version>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
<dependency>
<groupId>javax.xml.bind</groupId>
<artifactId>jaxb-api</artifactId>
<version>2.3.1</version>
</dependency>
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
<version>4.5.13</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
<!-- https://mvnrepository.com/artifact/com.google.code.gson/gson -->
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>2.9.0</version>
</dependency>
The WebSecurityConfigurerAdapter implementation
#Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
private final AppUserDetailService appUserDetailService;
private final Filter jwtAuthFilter;
#Autowired
public SecurityConfig(AppUserDetailService appUserDetailService, Filter jwtAuthFilter){
this.appUserDetailService = appUserDetailService;
this.jwtAuthFilter = jwtAuthFilter;
}
#Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(appUserDetailService);
}
#Override
protected void configure(HttpSecurity http) throws Exception {
.antMatchers("/auth/**","/oauth/**", "/topic/**", "/socket/**", "/app/**").permitAll()
.antMatchers("/api/**").authenticated()
.antMatchers("/**").permitAll().and()
.addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class);
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
}
#Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
#Override
#Bean
public AuthenticationManager authenticationManagerBean() throws Exception{
return super.authenticationManagerBean();
}
}
JWTAuthFilter
#Slf4j
#Component
public class JwtAuthFilter extends OncePerRequestFilter {
private final JWTUtilService jwtUtil;
public JwtAuthFilter(JWTUtilService jwtUtil) {
this.jwtUtil = jwtUtil;
}
#Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
String token = getAuthToken(request);
try{
if(token != null && !token.isBlank()){
String username = jwtUtil.extractUsername(token);
setSecurityContext(username);
}
}catch (Exception e){
log.error("No valid Token found!", e);
}
filterChain.doFilter(request, response);
}
private void setSecurityContext(String username) {
UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(username, "", List.of());
SecurityContextHolder.getContext().setAuthentication(authToken);
}
private String getAuthToken(HttpServletRequest request) {
String authHeader = request.getHeader("Authorization");
if(authHeader != null){
return authHeader.replace("Bearer", "").trim();
}
return null;
}
}
AppUserDetailsService implementation
#Service
public class AppUserDetailService implements UserDetailsService {
private final AppUserRepo appUserRepo;
private final BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
private final JWTUtilService jwtUtilService;
public AppUserDetailService(AppUserRepo appUserRepo, JWTUtilService jwtUtilService) {
this.appUserRepo = appUserRepo;
this.jwtUtilService = jwtUtilService;
}
#Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
return appUserRepo.findByUsername(username)
.map(appUser -> User
.withUsername(username)
.password(appUser.getPassword())
.authorities("user")
.build())
.orElseThrow(()-> new UsernameNotFoundException("Username does not exist: "+username));
}
public String registerUser(AppUserDTO user) {
if(!userExisting(user)){
user.setPassword(encoder.encode(user.getPassword()));
appUserRepo.save(user);
return jwtUtilService.createToken(new HashMap<>(), user.getUsername());
}else{
throw new UserExistsException("User is currently existing.");
}
}
public boolean userExisting(AppUserDTO user){
return appUserRepo.findByUsername(user.getUsername()).isPresent();
}
}
WebSocketMessageBrokerConfigurer implementation
#Configuration
#EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
#Override
public void configureMessageBroker(MessageBrokerRegistry config) {
config.enableSimpleBroker("/topic");
config.setApplicationDestinationPrefixes("/app");
}
#Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/socket").setAllowedOriginPatterns("*");
}
}
Here my simplified version of the implementation of the React frontend
Top Level Component
import {StompSessionProvider} from "react-stomp-hooks";
<StompSessionProvider url={"ws://localhost:8080/socket"} topics={['/topic/progress']} onConnect={()=>{console.log("Connected")}} onDisconnect={()=>{console.log("Disconnected")}} onError={(err)=>{console.log(err)}}>
<Home/>
</StompSessionProvider>
Home Component
import {useSubscription} from "react-stomp-hooks";
export default function ZapContinousHome() {
useSubscription("/topic/progress", (message) => setMessage(message.body));
return(
<h1>Home</h1>
)
}
I have created an authorization server using the new spring authorization server module. I am able to get the token successfully but when I try to use the token against a protected endpoint with hasAuthority() I get forbidden 403 error. Below my pom.xml file
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.6.1</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.erycoking</groupId>
<artifactId>auth-service</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>auth-service</name>
<description>Auth Service</description>
<properties>
<java.version>11</java.version>
<spring-cloud.version>2021.0.0</spring-cloud.version>
<jhipster-dependencies.version>7.0.1</jhipster-dependencies.version>
<liquibase.version>4.6.1</liquibase.version>
</properties>
<dependencies>
<dependency>
<groupId>tech.jhipster</groupId>
<artifactId>jhipster-framework</artifactId>
<version>7.4.0</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-oauth2-authorization-server</artifactId>
<version>0.2.0</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct</artifactId>
<version>1.4.2.Final</version>
</dependency>
<!-- Jackson Configurations -->
<dependency>
<groupId>com.fasterxml.jackson.module</groupId>
<artifactId>jackson-module-jaxb-annotations</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-hibernate5</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-hppc</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jsr310</artifactId>
</dependency>
<dependency>
<groupId>org.zalando</groupId>
<artifactId>problem-spring-web</artifactId>
<version>0.26.0</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.liquibase</groupId>
<artifactId>liquibase-core</artifactId>
<!-- Inherited version from Spring Boot can't be used because of regressions -->
<version>${liquibase.version}</version>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>11</source>
<target>11</target>
<annotationProcessorPaths>
<path>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>1.4.2.Final</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
<plugin>
<groupId>org.liquibase</groupId>
<artifactId>liquibase-maven-plugin</artifactId>
<version>${liquibase.version}</version>
</plugin>
</plugins>
</build>
</project>
Below is my Authorization Server config
#Configuration(proxyBeanMethods = false)
public class AuthServerConfig {
private final DataSource dataSource;
private final AuthProperties authProps;
private final TokenSettings tokenSettings;
public AuthServerConfig(DataSource dataSource, AuthProperties authProps, TokenSettings tokenSettings) {
this.dataSource = dataSource;
this.authProps = authProps;
this.tokenSettings = tokenSettings;
}
#Bean
public JdbcTemplate jdbcTemplate() {
return new JdbcTemplate(dataSource);
}
#Bean
#Order(Ordered.HIGHEST_PRECEDENCE)
public SecurityFilterChain authServerSecurityFilterChain(HttpSecurity http) throws Exception {
OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http);
return http.formLogin(Customizer.withDefaults()).build();
}
#Bean
public RegisteredClientRepository registeredClientRepository(JdbcTemplate jdbcTemplate) {
JdbcRegisteredClientRepository clientRepository = new JdbcRegisteredClientRepository(jdbcTemplate);
clientRepository.save(webClient());
return clientRepository;
}
private RegisteredClient webClient() {
return RegisteredClient.withId("98a9104c-a9c7-4d7c-ad03-ec61bcfeab36")
.clientId(authProps.getClientId())
.clientName(authProps.getClientName())
.clientSecret(authProps.getClientSecret())
.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
.authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS)
.authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN)
.redirectUri("http://127.0.0.1:8080/authorized")
.scope("create").scope("read").scope("write").scope("update").scope("delete")
.tokenSettings(tokenSettings)
.build();
}
#Bean
public OAuth2AuthorizationService authorizationService(JdbcTemplate jdbcTemplate,
RegisteredClientRepository registeredClientRepository) {
return new JdbcOAuth2AuthorizationService(jdbcTemplate, registeredClientRepository);
}
#Bean
public OAuth2AuthorizationConsentService authorizationConsentService(JdbcTemplate jdbcTemplate,
RegisteredClientRepository registeredClientRepository) {
return new JdbcOAuth2AuthorizationConsentService(jdbcTemplate, registeredClientRepository);
}
#Bean
public JWKSource<SecurityContext> jwkSource() {
RSAKey rsaKey = generateRsa();
JWKSet jwkSet = new JWKSet(rsaKey);
return (jwkSelector, securityContext) -> jwkSelector.select(jwkSet);
}
private static RSAKey generateRsa() {
KeyPair keyPair = generateRsaKey();
RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic();
RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate();
return new RSAKey.Builder(publicKey)
.privateKey(privateKey)
.keyID(UUID.randomUUID().toString())
.build();
}
private static 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;
}
#Bean
public ProviderSettings providerSettings() {
return ProviderSettings.builder()
.issuer(authProps.getIssuerUri())
.build();
}
}
And this is my Security Config
#EnableWebSecurity
#EnableGlobalMethodSecurity(securedEnabled = true, prePostEnabled = true)
public class SecurityConfig {
private final UserDetailsService userDetailsService;
public SecurityConfig(UserDetailsService userDetailsService) {
this.userDetailsService = userDetailsService;
}
#Bean
SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/management/**").permitAll()
.antMatchers("/h2-console/**").permitAll()
//this does not work
.antMatchers(HttpMethod.POST, "/auth/user").hasAuthority(AuthoritiesConstants.ADMIN)
//this does not work
.antMatchers(HttpMethod.GET, "/auth/user").hasAuthority("SCOPE_read")
.anyRequest().authenticated()
.and()
.csrf().disable()
.headers().frameOptions().disable()
.and()
.formLogin(withDefaults())
.userDetailsService(userDetailsService);
return http.build();
}
#Bean
public PasswordEncoder delegatingPasswordEncoder() {
Map<String, PasswordEncoder> encoders = new HashMap<>();
encoders.put("bcrypt", new BCryptPasswordEncoder());
DelegatingPasswordEncoder passwordEncoder = new DelegatingPasswordEncoder("bcrypt", encoders);
passwordEncoder.setDefaultPasswordEncoderForMatches(new BCryptPasswordEncoder());
return passwordEncoder;
}
#Bean
public TokenSettings tokenSettings() {
return TokenSettings.builder()
.accessTokenTimeToLive(Duration.ofMinutes(1))
.refreshTokenTimeToLive(Duration.ofHours(24))
.build();
}
}
Here is my user detail service
#Service("userDetailsService")
public class CustomUserDetailsService implements UserDetailsService {
private final UserRepository userRepository;
public CustomUserDetailsService(UserRepository userRepository) {
this.userRepository = userRepository;
}
#Override
public UserDetails loadUserByUsername(String login) throws UsernameNotFoundException {
return userRepository.findOneWithRolesByEmailIgnoreCase(login)
.map(user -> createSpringSecurityUser(login, user))
.orElseThrow(() -> new UsernameNotFoundException("User not found"));
}
private org.springframework.security.core.userdetails.User createSpringSecurityUser(String lowercaseLogin, User user) {
if (!user.isActivated()) {
throw new UserNotActivatedException("User " + lowercaseLogin + " was not activated");
}
List<GrantedAuthority> grantedAuthorities = new ArrayList<>();
user.getRoles().forEach(e -> {
grantedAuthorities.add(new SimpleGrantedAuthority(e.getName()));
e.getPermissions().forEach(p -> grantedAuthorities.add(new SimpleGrantedAuthority(p.getName())));
});
return new org.springframework.security.core.userdetails.User(user.getEmail(), user.getPassword(), grantedAuthorities);
}
}
when using the token to make a request to an endpoint that requires authentication alone it succeeds but when I try using it on an endpoint that requires a role it fails.
What could be the issue?
Based on a brief discussion in comments to clarify your setup, it seems that you are configuring your authorization server as a regular secured application with the intention of using scopes as authorities. However, this is an incorrect usage of the authorization server.
Note: I have not seen any reference to an OAuth 2.0 client or resource server in your description, so I'm assuming you are trying to hit endpoints directly on the authorization server. If that's not the case, let me know.
There are three applications involved in an OAuth 2.0 protected setup:
Authorization server
Resource server
Client
Your configuration is only for #1 (as far as I can tell). The authorization server contains two filter chains and additionally a configuration for a single oauth client. The two filter chains do the following:
Secure endpoints provided by the authorization server framework
Secure the login endpoint(s) the user will interact with prior to using the authorization endpoint (/oauth2/authorize) to obtain an authorization code, which the client will later use to obtain an access token
The scopes you have configured would allow a user (resource owner) to grant an oauth client the ability to make a protected call to a resource server using an access token. Only when the client makes a call to a resource server will your configured scopes be used. When the user directly interacts with an endpoint on the authorization server using a browser, the configuration for form login is in play, which as I mentioned in comments, uses roles from your database.
See the SpringOne 2021 repository and presentation to understand how to take an application from an unsecured application to a secured one, and then see how we turn it into a resource server, which uses scopes as authorities.
The presentation demonstrates all three applications, though the focus is on the resource server, which matches closely what you are trying to accomplish with scopes as authorities.
Initially, I started the project with IntelliJ Idea Tomcat Local Server configuration (everything worked), but decided to use Spring Boot Application. I added the main class, changed pom.xml (delete spring-context and add spring-boot-starter-parent, spring-boot, spring-boot-starter-tomcat, spring-boot-starter-web, spring-boot-autoconfigure), after that application runs , GET-Method works, but POST - not supported. Help me please!!! Thank you!
Main.class
#SpringBootApplication(scanBasePackageClasses = {SpringConfig.class})
public class Main extends SpringBootServletInitializer {
public static void main(String[] args) {
SpringApplication.run(Main.class, args);
}
}
Pom.xml to springBoot
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.4.3</version>
<relativePath/>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot</artifactId>
<version>2.4.3</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>2.4.3</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-autoconfigure</artifactId>
<version>2.4.3</version>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>4.0.1</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.thymeleaf</groupId>
<artifactId>thymeleaf-spring5</artifactId>
<version>3.0.11.RELEASE</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-collections4</artifactId>
<version>4.4</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.11</version>
<scope>test</scope>
</dependency>
Main configuration class
#Configuration
#ComponentScan("ru")
#PropertySource("classpath:application.properties")
#EnableWebMvc
public class SpringConfig implements WebMvcConfigurer {
private final ApplicationContext applicationContext;
#Autowired
public SpringConfig(ApplicationContext applicationContext) {
this.applicationContext = applicationContext;
}
/*
* Dispatcher configuration for serving static resources
*/
#Override
public void addResourceHandlers(final ResourceHandlerRegistry registry) {
WebMvcConfigurer.super.addResourceHandlers(registry);
registry.addResourceHandler("/images/**").addResourceLocations("/images/");
registry.addResourceHandler("/webjars/bootstrap/4.6.0/css/**").addResourceLocations("/webjars/bootstrap/4.6.0/css/bootstrap.min.css");
registry.addResourceHandler("assets/select2-develop/**").addResourceLocations("/assets/");
registry.addResourceHandler("/css/**").addResourceLocations("/css/");
registry.addResourceHandler("/js/**").addResourceLocations("/js/");
}
/*
* Message externalization/internationalization
*/
#Bean
public ResourceBundleMessageSource messageSource() {
ResourceBundleMessageSource messageSource = new ResourceBundleMessageSource();
messageSource.setBasename("Messages");
return messageSource;
}
#Bean
public SpringResourceTemplateResolver templateResolver(){
// SpringResourceTemplateResolver automatically integrates with Spring's own
// resource resolution infrastructure, which is highly recommended.
SpringResourceTemplateResolver templateResolver = new SpringResourceTemplateResolver();
templateResolver.setApplicationContext(this.applicationContext);
templateResolver.setPrefix("/WEB-INF/views/");
templateResolver.setSuffix(".html");
templateResolver.setCharacterEncoding("UTF-8");
// HTML is the default value, added here for the sake of clarity.
templateResolver.setTemplateMode(TemplateMode.HTML);
// Template cache is true by default. Set to false if you want
// templates to be automatically updated when modified.
templateResolver.setCacheable(true);
return templateResolver;
}
#Bean
public SpringTemplateEngine templateEngine(){
// SpringTemplateEngine automatically applies SpringStandardDialect and
// enables Spring's own MessageSource message resolution mechanisms.
SpringTemplateEngine templateEngine = new SpringTemplateEngine();
templateEngine.setTemplateResolver(templateResolver());
// Enabling the SpringEL compiler with Spring 4.2.4 or newer can
// speed up execution in most scenarios, but might be incompatible
// with specific cases when expressions in one template are reused
// across different data types, so this flag is "false" by default
// for safer backwards compatibility.
templateEngine.setEnableSpringELCompiler(true);
return templateEngine;
}
#Override
public void configureViewResolvers(ViewResolverRegistry registry) {
ThymeleafViewResolver resolver = new ThymeleafViewResolver();
resolver.setTemplateEngine(templateEngine());
resolver.setCharacterEncoding("UTF-8");
registry.viewResolver(resolver);
}
#Bean
public DataSource dataSource() {
DriverManagerDataSource dataSource = new DriverManagerDataSource();
dataSource.setDriverClassName("org.postgresql.Driver");
dataSource.setUrl("jdbc:postgresql://localhost:5432/postgres");
dataSource.setUsername("over");
// dataSource.setPassword("postgres"); Можно установить пароль для базы данных.
return dataSource;
}
#Bean
public JdbcTemplate jdbcTemplate() {
return new JdbcTemplate(dataSource());
}
}
DispatcherSerlvet config
public class MySpringMvcDispatcherSerlvetIntitializer extends AbstractAnnotationConfigDispatcherServletInitializer {
#Override
protected Class<?>[] getRootConfigClasses() {
return null;
}
/* Класс знает теперь где находится spring конфигурация */
#Override
protected Class<?>[] getServletConfigClasses() {
return new Class[]{SpringConfig.class};
}
/* Все http запросы от пользователя посылаем на dispatcher servlet */
#Override
protected String[] getServletMappings() {
return new String[]{"/"};
}
#Override
public void onStartup(ServletContext aServletContext) throws ServletException {
super.onStartup(aServletContext);
registerCharacterEncodingFilter(aServletContext);
registerHiddenFieldFilter(aServletContext);
}
private void registerHiddenFieldFilter(ServletContext aContext) {
aContext.addFilter("hiddenHttpMethodFilter",
new HiddenHttpMethodFilter()).addMappingForUrlPatterns(null ,true, "/*");
}
private void registerCharacterEncodingFilter(ServletContext aContext) {
EnumSet<DispatcherType> dispatcherTypes = EnumSet.of(DispatcherType.REQUEST, DispatcherType.FORWARD);
CharacterEncodingFilter characterEncodingFilter = new CharacterEncodingFilter();
characterEncodingFilter.setEncoding("UTF-8");
characterEncodingFilter.setForceEncoding(true);
FilterRegistration.Dynamic characterEncoding = aContext.addFilter("characterEncoding", characterEncodingFilter);
characterEncoding.addMappingForUrlPatterns(dispatcherTypes, true, "/*");
}
}
This is my exeption :
org.springframework.web.servlet.mvc.support.DefaultHandlerExceptionResolver.logException - Resolved [org.springframework.web.HttpRequestMethodNotSupportedException: Request method 'POST' not supported]
Controller
#RequestMapping("/categories")
#Controller
public class CategoriesController {
private CategoriesDao categoriesDAO;
#Autowired
public void setCategoriesDAO(CategoriesDao categoriesDAO)
{
this.categoriesDAO = categoriesDAO;
}
#GetMapping
public String index(Model model) {
model.addAttribute("category", new Category());
model.addAttribute("categories", categoriesDAO.index());
return "categories/index";
}
#PostMapping
public String addCategory(#ModelAttribute("category") #Valid Category category,
BindingResult bindingResult, Model model) {
if(bindingResult.hasErrors()) {
model.addAttribute("categories", categoriesDAO.index());
return "categories/index";
}
categoriesDAO.addCategory(category);
return "redirect:categories";
}
#DeleteMapping("/{id}")
public String deleteCategory(#PathVariable("id") int id) {
categoriesDAO.deleteCategory(id);
return "redirect:/categories";
}
#GetMapping("/{id}/edit")
public String editCategory(Model model, #PathVariable("id") int id) {
model.addAttribute("editCategory", categoriesDAO.editCategory(id));
return "categories/edit";
}
#PatchMapping("/{id}")
public String updateCategory(#ModelAttribute("editCategory") Category updateCategory,
#PathVariable("id") int id) {
categoriesDAO.updateCategory(id, updateCategory);
return "redirect:{id}/edit";
}
}
To implement the configure method in the main class you could do:
#SpringBootApplication
public class Main extends SpringBootServletInitializer {
public static void main(String[] args) {
SpringApplication.run(Main.class, args);
}
#Override
protected SpringApplicationBuilder configure(
SpringApplicationBuilder builder) {
return builder.sources(Main.class);
}
}
I think these dependencies should be enough (assuming you are using Thymeleaf):
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.4.3</version>
<relativePath/>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>2.4.3</version>
</dependency>
<dependency>
<groupId>org.thymeleaf</groupId>
<artifactId>thymeleaf-spring5</artifactId>
<version>3.0.11.RELEASE</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-collections4</artifactId>
<version>4.4</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.11</version>
<scope>test</scope>
</dependency>
EDIT: Your ModelAttribute 'Category' might not be parsed successfully. Either the mapping fails, or maybe the validation? You might find out by adding an error handler and/or debugging the sessions.
Other ideas: Use #RestController instead of #Controller. Try to make the request mappings more explicit. Instead of on the class, put one on each method:
#RequestMapping(value = "/categories", produces = "application/json", method=RequestMethod.GET)
public String index(Model model) {
...
#RequestMapping(value = "/categories", produces = "application/json", method=RequestMethod.POST)
public String addCategory(#ModelAttribute("category") #Valid Category category,
BindingResult bindingResult, Model model) {
...
Thanks everyone! I found answer on my question. It was some problems with HiddenHttpMethodFilter. I just added this code in my Spring Config and extra dependencies was . It stared to work!!!
public FilterRegistrationBean<HiddenHttpMethodFilter> hiddenHttpMethodFilter() {
FilterRegistrationBean<HiddenHttpMethodFilter> filterRegistrationBean =
new FilterRegistrationBean<HiddenHttpMethodFilter>(new HiddenHttpMethodFilter());
filterRegistrationBean.setUrlPatterns(Collections.singletonList("/*"));
return filterRegistrationBean;
}
restTemplate throw UnknownHostException when use service-name
I have add bean restTemplate
#Configuration
public class SpringCloudConfig {
#LoadBalanced
#Bean
public RestTemplate restTemplate() {
return new RestTemplate();
}
}
I use the Spring-cloud Greenwich.SR3 in parent pom
dependencies:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-oauth2</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
</dependency>
yml :
#OAuth
security:
oauth2:
resource:
loadBalanced: true
token-info-uri: http://FLY-AUTH/oauth/check_token
client:
client-id: sanke
client-secret: sanke
scope: all
OAuth info in yml
Modify ResourceServerConfig file
#Configuration
#EnableResourceServer
#AllArgsConstructor
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
private final EntryPointUnauthorizedHandler entryPointUnauthorizedHandler;
private final MyAccessDeniedHandler myAccessDeniedHandler;
private final RemoteTokenServices remoteTokenServices;
private final RestTemplate restTemplate;
#Override
public void configure(HttpSecurity http) throws Exception {
http
.csrf().disable()
.cors()
.and()
.authorizeRequests()
.antMatchers(HttpMethod.OPTIONS, "/**").permitAll()
.antMatchers("/auth/**").permitAll()
.anyRequest().permitAll()
.and()
.exceptionHandling()
.authenticationEntryPoint(entryPointUnauthorizedHandler)
.accessDeniedHandler(myAccessDeniedHandler);
}
#Override
public void configure(ResourceServerSecurityConfigurer resources) {
DefaultAccessTokenConverter accessTokenConverter = new DefaultAccessTokenConverter();
UserAuthenticationConverter userTokenConverter = new FlyUserAuthenticationConverter();
accessTokenConverter.setUserTokenConverter(userTokenConverter);
//Config restTemplete
remoteTokenServices.setRestTemplate(restTemplate);
remoteTokenServices.setAccessTokenConverter(accessTokenConverter);
resources.tokenServices(remoteTokenServices);
resources.authenticationEntryPoint(entryPointUnauthorizedHandler);
}
}