I'm creating an API with Spring Boot and Spring Security. I already created some basic authentication mechanism. And currently facing some unknown problem with authorization of requests.
Here is my Configuration class:
// removed for brevity
#EnableWebSecurity
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
private final CustomUserDetailsService customUserDetailsService;
private final JwtTokenFilter jwtTokenFilter;
private final CustomAuthenticationProvider customAuthenticationProvider;
public SecurityConfiguration(CustomUserDetailsService customUserDetailsService,
JwtTokenFilter jwtTokenFilter,
CustomAuthenticationProvider customAuthenticationProvider) {
this.customUserDetailsService = customUserDetailsService;
this.jwtTokenFilter = jwtTokenFilter;
this.customAuthenticationProvider = customAuthenticationProvider;
}
#Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
// todo: provide an authenticationProvider for authenticationManager
/* todo:
In most use cases authenticationProvider extract user info from database.
To accomplish that, we need to implement userDetailsService (functional interface).
Here username is an email.
* */
auth.userDetailsService(customUserDetailsService);
auth.authenticationProvider(customAuthenticationProvider);
}
#Override
protected void configure(HttpSecurity http) throws Exception {
// Enable CORS and disable CSRF
http = http.cors().and().csrf().disable();
// Set session management to Stateless
http = http.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and();
// Set unauthorized requests exception handler
http = http
.exceptionHandling()
.authenticationEntryPoint(
(request, response, ex) -> {
response.sendError(
HttpServletResponse.SC_UNAUTHORIZED,
ex.getMessage()
);
}
)
.and();
// Set permissions and endpoints
http.authorizeRequests()
.antMatchers("/api/v1/auth/**").permitAll()
.antMatchers("/api/v1/beats/**").hasRole("ADMIN")
.anyRequest().authenticated();
http.addFilterBefore(jwtTokenFilter,
UsernamePasswordAuthenticationFilter.class);
}
#Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
// Used by spring security if CORS is enabled.
#Bean
public CorsFilter corsFilter() {
UrlBasedCorsConfigurationSource source =
new UrlBasedCorsConfigurationSource();
CorsConfiguration config = new CorsConfiguration();
config.setAllowCredentials(true);
config.addAllowedOrigin("*");
config.addAllowedHeader("*");
config.addAllowedMethod("*");
source.registerCorsConfiguration("/**", config);
return new CorsFilter(source);
}
#Override #Bean
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
#Bean
GrantedAuthorityDefaults grantedAuthorityDefaults() {
return new GrantedAuthorityDefaults(""); // Remove the ROLE_ prefix
}
}
To check if user has rights to access resource, I use info from JWT payload. To do so I have a filter class:
// removed for brevity
#Component
public class JwtTokenFilter extends OncePerRequestFilter {
private final static Logger logger = LoggerFactory.getLogger(JwtTokenFilter.class);
private final JwtTokenUtil jwtTokenUtil;
private final CustomUserDetailsService customUserDetailsService;
public JwtTokenFilter(JwtTokenUtil jwtTokenUtil,
CustomUserDetailsService customUserDetailsService) {
this.jwtTokenUtil = jwtTokenUtil;
this.customUserDetailsService = customUserDetailsService;
}
#Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
final String header = request.getHeader(HttpHeaders.AUTHORIZATION);
if (header == null || header.isEmpty() || !header.startsWith("Bearer ")) {
logger.error("Authorization header missing");
filterChain.doFilter(request, response);
return;
}
final String token = header.split(" ")[1].trim();
if (!jwtTokenUtil.validate(token)) {
filterChain.doFilter(request, response);
return;
}
UserDetails userDetails = customUserDetailsService.loadUserByUsername(token);
if (userDetails == null)
throw new ServletException("Couldn't extract user from JWT credentials");
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(
userDetails, userDetails.getPassword(), userDetails.getAuthorities());
authentication.setDetails(
new WebAuthenticationDetailsSource().buildDetails(request)
);
SecurityContextHolder.getContext().setAuthentication(authentication);
filterChain.doFilter(request, response);
}
}
To represent UserDetails, I've implemented CustomUserDetails and CustomUserDetailsService classes:
#Data
#NoArgsConstructor
public class CustomUserDetails implements UserDetails {
private Long userId;
private Long profileId;
private String email;
private String password;
private String fullName;
private String nickname;
private String avatar;
private String phoneNumber;
private ProfileState profileState;
private Collection<? extends GrantedAuthority> grantedAuthorities;
public static CustomUserDetails fromUserAndProfileToMyUserDetails(Profile profile) {
CustomUserDetails customUserDetails = new CustomUserDetails();
customUserDetails.setUserId(profile.getUser().getId());
customUserDetails.setEmail(profile.getUser().getEmail());
customUserDetails.setPassword(profile.getUser().getPassword());
customUserDetails.setProfileId(profile.getId());
customUserDetails.setFullName(profile.getFullName());
customUserDetails.setNickname(profile.getNickname());
customUserDetails.setAvatar(profile.getAvatar());
customUserDetails.setPhoneNumber(profile.getPhoneNumber());
customUserDetails.setProfileState(profile.getState());
return customUserDetails;
}
#Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return grantedAuthorities;
}
#Override
public String getPassword() {
return password;
}
#Override
public String getUsername() {
return nickname;
}
#Override
public boolean isAccountNonExpired() {
return false;
}
#Override
public boolean isAccountNonLocked() {
return false;
}
#Override
public boolean isCredentialsNonExpired() {
return false;
}
#Override
public boolean isEnabled() {
return false;
}
}
CustomUserDetailsService.java:
#Component
public class CustomUserDetailsService implements UserDetailsService {
private Logger logger = LoggerFactory.getLogger(CustomUserDetailsService.class);
private final ProfileRepository profileRepository;
private final JwtTokenUtil jwtTokenUtil;
public CustomUserDetailsService(ProfileRepository profileRepository, JwtTokenUtil jwtTokenUtil) {
this.profileRepository = profileRepository;
this.jwtTokenUtil = jwtTokenUtil;
}
#Override
public UserDetails loadUserByUsername(String token) throws UsernameNotFoundException {
if (token == null || token.isEmpty()) throw new IllegalArgumentException("Token cannot be null or empty");
try {
final String nickname = jwtTokenUtil.getNickname(token);
Profile profile = profileRepository
.findByNickname(nickname)
.orElseThrow(() -> new UsernameNotFoundException(
String.format("User: %s not found", token)
));
logger.info(String.format("Extracted Profile: %s", profile));
CustomUserDetails customUserDetails = CustomUserDetails.fromUserAndProfileToMyUserDetails(profile);
List<GrantedAuthority> authorities = new ArrayList<>(Collections.emptyList());
authorities.add(new SimpleGrantedAuthority(profile.getType().getValue()));
customUserDetails.setGrantedAuthorities(authorities);
return customUserDetails;
} catch (Exception e) {
logger.error("Wasn't able to load user `{}`. Exception occurred `{}`", token, e.getMessage());
return null;
}
}
}
Here is the controller that I want to access:
#RestController
#RequestMapping("/api/beats")
public class BeatController {
private static final Logger logger = LogManager.getLogger(BeatController.class);
private final BeatService beatService;
public BeatController(BeatService beatService) {
this.beatService = beatService;
}
#GetMapping("{id}")
public Object getBeat(#PathVariable Long id) {
try {
return beatService.findById(id);
} catch (Exception e) {
logger.error("Can't find beat with id " + id);
return new ResponseEntity<>(new DefaultResponseDto("failed", e.getMessage()), HttpStatus.INTERNAL_SERVER_ERROR);
}
}
#GetMapping
public Object getBeats(#RequestParam String filter, #RequestParam String page) {
try {
return beatService.findAll();
} catch (Exception e) {
logger.error("Can't find beats");
return new ResponseEntity<>(new DefaultResponseDto("failed", e.getMessage()), HttpStatus.INTERNAL_SERVER_ERROR);
}
}
#PostMapping
public Object createBeat(#RequestBody BeatDto beatDto) {
try {
beatDto.setId(null);
return beatService.save(beatDto);
} catch (Exception e) {
logger.error("Can't create new Beat");
return new ResponseEntity<>(new DefaultResponseDto("failed", e.getMessage()), HttpStatus.INTERNAL_SERVER_ERROR);
}
}
#PutMapping("{id}")
public Object updateBeat(#PathVariable Long id, #RequestBody BeatDto newBeat) {
try{
BeatDto oldBeat = beatService.findById(id);
if (oldBeat != null) {
newBeat.setId(id);
} else {
throw new Exception();
}
return beatService.save(newBeat);
} catch (Exception e) {
return new ResponseEntity<>(new DefaultResponseDto("failed", e.getMessage()), HttpStatus.INTERNAL_SERVER_ERROR);
}
}
#DeleteMapping("{id}")
public Object deleteBeat(#PathVariable Long id) {
try {
return beatService.deleteById(id);
} catch (Exception e) {
return new ResponseEntity<>(new DefaultResponseDto("failed", e.getMessage()), HttpStatus.INTERNAL_SERVER_ERROR);
}
}
}
So, I make a request, provide it with and correct Authorization header and access token. It gets a user from DB and fetches GrantedAuthority. Last steps are:
It sets authentication object in SecurityContext.
Goes further in FilterChain.
But it doens't reach controller, and it doens't throw any exceptions. Only responses me with 403. May be I forgot something to setup, or problem might be somewehere else? Guide me please.
So finally figured out what was the problem. Main advices that helped me here:
All methods in CustomUserDetails service that were returning false return true. (Advice from M. Deinum)
Turned on spring framework security logs with: logging.level.org.springframework.security=TRACE.
This helped me to trace an exception, that FilterChain was throwing.
Thanks to Marcus Hert da Coregio.
What I changed to fix a problem? First I updated #RequestMapping mismatch in BeatController. Stack trace showed me that while it was properly fetching user Role from DB, it failed to match my Role and the one I wrote in Configuration class. By default, it add "ROLE_" prefix before the actual role name we provide. I thought that defining this bean changes this behavior:
GrantedAuthorityDefaults grantedAuthorityDefaults() {
return new GrantedAuthorityDefaults(""); // Remove the ROLE_ prefix
}
Turns out that it doesn't effect to prefixing behavior, so it was adding "ROLE_" before the "ADMIN" role name I provided. Adding "ROLE_" prefix while authenticating request fixed problem:
FROM
authorities.add(new SimpleGrantedAuthority(profile.getType().getValue()));
TO
authorities.add(new SimpleGrantedAuthority("ROLE_" + profile.getType().getValue()));
Additionally I cleaned build and rebuild the project with gradle. Thanks to all people that helped!
Related
everyone!
I making a defense against password brute force.
I successfully handle AuthenticationFailureBadCredentialsEvent when the user writes the right login and wrong password. But the problem is that I want to return JSON with two fields
{
message : '...' <- custom message
code : 'login_failed'
}
The problem is that it returns standart forbidden exception, but I need custom json.
#Log4j2
#Component
#RequiredArgsConstructor
public class AuthenticationAttemptsHandler {
protected final MessageSource messageSource;
private final AuthenticationAttemptsStore attemptsStore;
private final UserDetailsService userDetailsService;
private final UserDetailsLockService userDetailsLockService;
#EventListener
public void handleFailure(AuthenticationFailureBadCredentialsEvent event) {
val authentication = event.getAuthentication();
val userDetails = findUserDetails(authentication.getName());
userDetails.ifPresent(this::failAttempt);
}
private Optional<UserDetails> findUserDetails(String username) {
...
}
private void failAttempt(UserDetails details) {
val username = details.getUsername();
val attempt = attempt(loginAttemptsProperties.getResetFailuresInterval());
int failures = attemptsStore.incrementFailures(username, attempt);
if (failures >= 2) {
Instant lockedUntil = Instant.now().plus(loginAttemptsProperties.getLockDuration());
userDetailsLockService.lockUser(username, lockedUntil);
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("HH:mm");
String date = formatter.format(lockedUntil);
String message = String.format("Account will locked till %s", date);
throw new SecurityException(message);
//FailAttemptsExceptionResponse response = new FailAttemptsExceptionResponse(message, //
//"login_ failed"); <---- tryed return entity from this method. Does not work.
// return new ResponseEntity<>(response,HttpStatus.FORBIDDEN);
} else {
String message = String.format("You have %s attempts.", (3 - failures));
// FailAttemptsExceptionResponse response = new FailAttemptsExceptionResponse(message,
"login_ failed");
throw new SecurityException(message);
// return new ResponseEntity<>(response,HttpStatus.FORBIDDEN);
}
}
}
RuntimeException returns 500 status? but I need forbidden
public class SecurityException extends RuntimeException {
private static final long serialVersionUID = 1L;
public SecurityException(String msg) {
super(msg);
}
}
Responce model
public class FailAttemptsExceptionResponse {
String message;
String code;
public FailAttemptsExceptionResponse(String message, String code) {
super();
this.message = message;
this.code = code;
}
public String getMessage() {
return message;
}
public String getCode() {
return code;
}
}
Tried to handle SecurityException and then returns model? but it does not work
#ControllerAdvice
public class SeurityAdvice extends ResponseEntityExceptionHandler {
#ExceptionHandler(SecurityException.class)
public ResponseEntity<FailAttemptsExceptionResponse> handleNotFoundException(SecurityException ex) {
FailAttemptsExceptionResponse exceptionResponse = new FailAttemptsExceptionResponse(ex.getMessage(),
"login_ failed");
return new ResponseEntity<FailAttemptsExceptionResponse>(exceptionResponse,
HttpStatus.NOT_ACCEPTABLE);
}
}
I successfully handle AuthenticationFailureBadCredentialsEvent, but how can I return JSON response model from the handler with a custom message?
#ControllerAdvice
public class SeurityAdvice extends ResponseEntityExceptionHandler {
#ExceptionHandler(SecurityException.class)
public ResponseEntity<FailAttemptsExceptionResponse> handleNotFoundException(SecurityException ex, HttpServletResponse response) {
FailAttemptsExceptionResponse exceptionResponse = new FailAttemptsExceptionResponse(ex.getMessage(),
"login_ failed");
response.setStatus(HttpServletResponse.SC_FORBIDDEN);
return new ResponseEntity<FailAttemptsExceptionResponse>(exceptionResponse,
HttpStatus.NOT_ACCEPTABLE);
}
}
maybe you need to add HttpServletResponse and set the http status.
Register the entry point
As mentioned, I do it with Java Config. I just show the relevant configuration here, there should be other configuration such as session stateless, etc.
#Configuration
#EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
http.exceptionHandling().authenticationEntryPoint(new CustomEntryPoint());
}
}
U can create AuthenticationEntryPoint.
Короч тут почитай xD
Handle spring security authentication exceptions with #ExceptionHandler
I am having a challenge extracting original user details from the LdapUserDetailsImpl such as the getUsername() returns null
I have the following Java classes
Custom User Class
public class AppUserDetails extends LdapUserDetailsImpl {
public AppUserDetails() {
super();
}
private String mail; //mail
}
Custom User Details Mapper
public class AppUserDetailsContextMapper extends LdapUserDetailsMapper {
public AppUserDetailsContextMapper() {
super();
}
#Override
public AppUserDetails mapUserFromContext(DirContextOperations ctx, String username, Collection<? extends GrantedAuthority> authorities) {
UserDetails details = super.mapUserFromContext(ctx, username, authorities);
String mail = ctx.getStringAttribute("mail");
AppUserDetails appUserDetails = new AppUserDetails();
appUserDetails.setMail(mail);
return appUserDetails;
}
}
Web security configuration
#Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
#Autowired
Environment env;
#Bean
public UserDetailsContextMapper userDetailsContextMapper() {
return new AppUserDetailsContextMapper();
}
#Bean
public ActiveDirectoryLdapAuthenticationProvider activeDirectoryLdapAuthenticationProvider() {
ActiveDirectoryLdapAuthenticationProvider provider =
new ActiveDirectoryLdapAuthenticationProvider(
env.getRequiredProperty("spring.ldap.domain"),
env.getRequiredProperty("spring.ldap.urls")
);
provider.setConvertSubErrorCodesToExceptions(true);
provider.setUseAuthenticationRequestCredentials(true);
provider.setUserDetailsContextMapper(userDetailsContextMapper());
return provider;
}
#Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth
.authenticationProvider(activeDirectoryLdapAuthenticationProvider())
.userDetailsService(userDetailsService());
}
#Bean
public AuthenticationManager authenticationManager() {
return new ProviderManager(
Collections
.singletonList(activeDirectoryLdapAuthenticationProvider())
);
}
}
However I am having a serious challenge trying to getting Custom User Details in the controller:
#Controller
public class HomeController {
#GetMapping(value = {"/home"})
public String home(Model model) {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
System.out.println(authentication.getPrincipal());
AppUserDetails appUserDetails = (AppUserDetails) authentication.getPrincipal();
System.out.println(appUserDetails);
return "home";
}
}
I am getting a NullPointer exception if I try to any property from the LdapUserDetailsImpl class. However, I am accurately getting all the properties from the AppUserDetails - which extends the LdapUserDetailsImpl class.
Where might I be missing it?
What I'm trying to implement is to make my multitenant app workspace aware. With this I mean that besides username and password, I'm validating the workspace as well.
Before, I had (working) normal authentication (username and password) and a JWTFilter that is a OncePerRequestFilter.
What I did?
Extended UsernamePasswordAuthenticationToken: just to add the workspace
Extended AbstractUserDetailsAuthenticationProvider: defining my customPasswordEncoder and customUserDetailsService
Made a CustomUserDetailsService: instead of the loadByUsername I've made a loadUserByWorkspaceAndUsername
Configured the WebSecurity for the new extended classes
The outcome is always unauthorized :(
What I've tried?
While debugging the code never pass on the CustomAuthenticationFilter and that's the reason I'm focusing my efforts there. Really doesn't know what I'm doing wrong here. If you need any further information please shout.
Replacing the UsernamePasswordAuthenticationFilter using the addFilter(authenticationFilter())
.addFilterBefore(jwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class)
.addFilterAfter(authenticationFilter(), JwtFilter.class);
.addFilterBefore(jwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class)
.addFilterBefore(authenticationFilter(),UsernamePasswordAuthenticationFilter.class);
.addFilterBefore(authenticationFilter(), UsernamePasswordAuthenticationFilter.class)
.addFilterBefore(jwtAuthenticationFilter(),UsernamePasswordAuthenticationFilter.class);
A bit of code.
CustomAuthenticationToken
public class CustomAuthenticationToken extends UsernamePasswordAuthenticationToken {
private String workspace;
public CustomAuthenticationToken(final Object principal,
final Object credentials,
final String workspace) {
super(principal, credentials);
this.workspace = workspace;
}
public CustomAuthenticationToken(final Object principal,
final Object credentials,
final String workspace, Collection<? extends GrantedAuthority> authorities) {
super(principal, credentials, authorities);
this.workspace = workspace;
super.setAuthenticated(true);
}
public String getWorkspace() {
return this.workspace;
}
}
CustomAuthenticationFilter
public class CustomAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
private static final String SPRING_SECURITY_FORM_DOMAIN_KEY = "workspace";
#Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
throws AuthenticationException {
if (!request.getMethod().equals("POST")) {
throw new AuthenticationServiceException("Authentication method not supported: "
+ request.getMethod());
}
CustomAuthenticationToken authRequest = getAuthRequest(request);
setDetails(request, authRequest);
return this.getAuthenticationManager().authenticate(authRequest);
}
private CustomAuthenticationToken getAuthRequest(HttpServletRequest request) {
String username = obtainUsername(request);
String password = obtainPassword(request);
String domain = obtainDomain(request);
if (username == null) {
username = "";
}
if (password == null) {
password = "";
}
if (domain == null) {
domain = "";
}
username = username.trim();
return new CustomAuthenticationToken(username, password, domain);
}
private String obtainDomain(HttpServletRequest request) {
return request.getParameter(SPRING_SECURITY_FORM_DOMAIN_KEY);
}
}
CustomUserDetailsAuthenticationProvider
#Component
public class CustomUserDetailsAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider {
/**
* The plaintext password used to perform
* PasswordEncoder#matches(CharSequence, String)} on when the user is
* not found to avoid SEC-2056.
*/
private static final String USER_NOT_FOUND_PASSWORD = "userNotFoundPassword";
private final PasswordEncoder customPasswordEncoder;
private final CustomUserDetailsService customUserDetailsService;
private String userNotFoundEncodedPassword;
public CustomUserDetailsAuthenticationProvider(final PasswordEncoder customPasswordEncoder,
final CustomUserDetailsService customUserDetailsService) {
this.customPasswordEncoder = customPasswordEncoder;
this.customUserDetailsService = customUserDetailsService;
}
#Override
protected void additionalAuthenticationChecks(final UserDetails userDetails,
final UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
if (authentication.getCredentials() == null) {
logger.debug("Authentication failed: no credentials provided");
throw new BadCredentialsException(messages.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
}
final String presentedPassword = authentication.getCredentials().toString();
if (!customPasswordEncoder.matches(presentedPassword, userDetails.getPassword())) {
logger.debug("Authentication failed: password does not match stored value");
throw new BadCredentialsException(messages.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
}
}
#Override
protected UserDetails retrieveUser(final String username,
final UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
final CustomAuthenticationToken auth = (CustomAuthenticationToken) authentication;
UserDetails loadedUser;
try {
loadedUser = this.customUserDetailsService.loadUserByWorkspaceAndUsername(auth.getWorkspace(), auth.getPrincipal().toString());
} catch (UsernameNotFoundException notFound) {
if (authentication.getCredentials() != null) {
String presentedPassword = authentication.getCredentials().toString();
customPasswordEncoder.matches(presentedPassword, userNotFoundEncodedPassword);
}
throw notFound;
} catch (Exception repositoryProblem) {
throw new InternalAuthenticationServiceException(repositoryProblem.getMessage(), repositoryProblem);
}
if (loadedUser == null) {
throw new InternalAuthenticationServiceException("UserDetailsService returned null, which is an interface contract violation");
}
return loadedUser;
}
#Override
protected void doAfterPropertiesSet() throws Exception {
Assert.notNull(this.customUserDetailsService, "A UserDetailsService must be set");
this.userNotFoundEncodedPassword = this.customPasswordEncoder.encode(USER_NOT_FOUND_PASSWORD);
}
}
CustomUserDetailsServiceImpl
#Component
public class CustomUserDetailsServiceImpl implements CustomUserDetailsService {
private static final Logger LOGGER = LoggerFactory.getLogger(com.cliwise.security.workspace.CustomUserDetailsServiceImpl.class);
private final LoginAttemptService loginAttemptService;
private final UserRepository userRepository;
private final HttpServletRequest request;
public CustomUserDetailsServiceImpl(LoginAttemptService loginAttemptService, UserRepository userRepository, HttpServletRequest request) {
this.loginAttemptService = loginAttemptService;
this.userRepository = userRepository;
this.request = request;
}
#Override
public UserDetails loadUserByWorkspaceAndUsername(String workspace, String username) throws UsernameNotFoundException {
final User user = userRepository.findByUsernameOrEmailAndWorkspace(username, username, workspace)
.orElseThrow(() -> new UserNotFoundException("User not found with username or email : " + username));
return UserPrincipal.create(user);
}
}
The last but no least
WebSecurity
#EnableWebSecurity
public class WebSecurity extends WebSecurityConfigurerAdapter {
private final CustomAuthenticationEntryPoint unauthorizedHandler;
private final CustomUserDetailsAuthenticationProvider customUserDetailsAuthenticationProvider;
public WebSecurity(final CustomAuthenticationEntryPoint unauthorizedHandler,
final CustomUserDetailsAuthenticationProvider customUserDetailsAuthenticationProvider) {
this.unauthorizedHandler = unauthorizedHandler;
this.customUserDetailsAuthenticationProvider = customUserDetailsAuthenticationProvider;
}
#Override
public void configure(final AuthenticationManagerBuilder authenticationManagerBuilder) throws Exception {
authenticationManagerBuilder.authenticationProvider(customUserDetailsAuthenticationProvider);
}
#Bean
public CustomAuthenticationFilter authenticationFilter() throws Exception {
CustomAuthenticationFilter filter = new CustomAuthenticationFilter();
filter.setAuthenticationManager(authenticationManagerBean());
return filter;
}
#Bean(BeanIds.AUTHENTICATION_MANAGER)
#Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
#Override
public void configure(HttpSecurity http) throws Exception {
http
.httpBasic().disable()
.csrf().disable()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.exceptionHandling().authenticationEntryPoint(unauthorizedHandler)
.and()
.exceptionHandling().accessDeniedHandler(accessDeniedHandler())
.and()
.addFilterBefore(jwtAuthenticationFilter(), CustomAuthenticationFilter.class);
http
.authorizeRequests()
.antMatchers("/auth").permitAll()
.anyRequest()
.authenticated();
}
#Bean
public AccessDeniedHandler accessDeniedHandler() {
return new CustomAccessDeniedHandler();
}
#Bean
public JwtFilter jwtAuthenticationFilter() {
return new JwtFilter();
}
}
Thanks in advance for your time.
My understanding is that you are facing the problem in CustomUserDetailsAuthenticationProvider. Since you are extending AbstractUserDetailsAuthenticationProver you will get a default implementation for
public Authentication authenticate(Authentication authentication)
throws AuthenticationException;
See if its properly authenticating the authentication object, if not you will have to override the method and write your own implementation.
I cannot access the java.security.Principal authenticated by my CAS authentication system. serverWebExchange.getPrincipal() is allways empty.
In fact this is the implementation of DefaultServerWebExchange :
#Override
public <T extends Principal> Mono<T> getPrincipal() {
return Mono.empty();
}
Another implementation of ServerWebExchange is ServerWebExchangeDecorator and its documentation says:
Note: if the purpose for using a decorator is to override properties
like getPrincipal(), consider using ServerWebExchange.mutate()
instead.
So my first question is: If that is what I am supposed to do, at what point in my WebFlux Security configuration (below) am I supposed to use ServerWebExchange.mutate().getPrincipal() to override ServerWebExchange.getPrincipal()?
My second question is: How am I supposed to get the pre-authenticated non-reactive request Principal?
So far, the only way I have found to retrieve the request Principal from ServerWebExchange is using reflection. But obviously that is not what I am supposed to do.
#EnableWebFluxSecurity
#EnableReactiveMethodSecurity
#Slf4j
public class SecurityConfig {
#Bean
public SecurityWebFilterChain securitygWebFilterChain(ServerHttpSecurity http) {
return http
.csrf().disable()
.httpBasic().disable()
.formLogin().disable()
.logout().disable()
.authenticationManager(this.authenticationManager())
.securityContextRepository(this.securityContextRepository())
.authorizeExchange().pathMatchers("/public/**").permitAll()
.and().authorizeExchange().anyExchange().authenticated()
.and().build();
}
#Bean
ReactiveAuthenticationManager authenticationManager() {
return authentication -> {
log.debug("Autentication: " + authentication.toString());
if (authentication instanceof CustomPreAuthenticationToken) {
authentication.setAuthenticated(true);
}
return Mono.just(authentication);
};
}
#Bean
ServerSecurityContextRepository securityContextRepository() {
return new ServerSecurityContextRepository() {
#Override
public Mono<Void> save(ServerWebExchange serverWebExchange, SecurityContext securityContext) {
return null;
}
#Override
public Mono<SecurityContext> load(ServerWebExchange serverWebExchange) {
Principal nonReactivePrincipal = getPrincipalFromExchangeUsingReflection(serverWebExchange);
return Mono.just(new SecurityContextImpl(new CustomPreAuthenticationToken(nonReactivePrincipal.getName(), nonReactivePrincipal, AuthorityUtils.createAuthorityList("ROLE_USER") )));
}
};
}
private Principal getPrincipalFromExchangeUsingReflection(ServerWebExchange serverWebExchange) {
Principal principal = null;
try {
Field ServletServerHttpRequestField = serverWebExchange.getClass().getDeclaredField("request");
ServletServerHttpRequestField.setAccessible(true);
Object servletServerHttpRequest = ServletServerHttpRequestField.get(serverWebExchange);
Field httpServletRequestField = servletServerHttpRequest.getClass().getDeclaredField("request");
httpServletRequestField.setAccessible(true);
HttpServletRequest httpServletRequest = (HttpServletRequest) httpServletRequestField.get(servletServerHttpRequest);
principal = httpServletRequest.getUserPrincipal();
} catch (IllegalAccessException | NoSuchFieldException e) {
log.error(e.getMessage(), e);
}
return principal;
}
}
public class CustomPreAuthenticationToken extends UsernamePasswordAuthenticationToken {
public CustomPreAuthenticationToken(String key, Object principal, Collection<? extends GrantedAuthority> authorities) {
super(key, principal, authorities);
}
}
I have a spring boot application i have many endpoint in this application. When i am hitting endpoint simultaneously JSON response from two different endpoint got interchanged.
For example:
i am hitting /currency/list endpoint and /fee endpoint and i am getting /fee endpoint data in currency/list endpoint and vice versa.
I have no idea why this happening. If anybody can suggest why happening will be helpful.
Also I am using spring security token based auth in this project
CurrencyController.java
#RestController
#RequestMapping(value = UrlConstant.BASE_ADMIN_URI_V1)
#Api(value = "Admin Controller")
#Scope("request")
public class CurrencyController {
public static final Logger logger = LoggerFactory.getLogger(CurrencyController.class);
#Autowired
private LocaleService localService;
#RequestMapping(value = UrlConstant.CURRENCY_LIST_FOR_MARKET, method = RequestMethod.GET)
public ResponseEntity<Object> getCurrencyListForMarket() {
List<Currency> currencyList = currencyService.getCurrencyListForMarket();
ObjectMapper mapper = new ObjectMapper();
try {
String stringList = mapper.writeValueAsString(currencyList);
logger.debug("all currency list as String: {}", stringList);
} catch (JsonProcessingException e) {
logger.debug("error in currency list: {}", e.getMessage());
e.printStackTrace();
}
return ResponseHandler.response(HttpStatus.OK, false, localService.getMessage("currency.list.success"),
currencyList);
}
}
AdminController.java
#RestController
#RequestMapping(value = UrlConstant.BASE_ADMIN_URI_V1)
#Api(value = "Admin Controller")
#Scope("request")
public class AdminController {
#Autowired
private LocaleService localeService;
#Autowired
private FeeService feeService;
#RequestMapping(value = UrlConstant.TRADING_FEES, method = RequestMethod.GET)
public ResponseEntity<Object> getTradingFees() {
TradingFee fee = tradingFeeService.getTradingFee();
return ResponseHandler.response(HttpStatus.OK, true,
localeService.getMessage("admin.transaction.fees.found.success"), fee);
}
}
TokenAuthenticationFilter.java
public class TokenAuthenticationFilter extends GenericFilterBean {
#Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
final HttpServletRequest httpRequest = (HttpServletRequest) request;
// extract token from header
String token = httpRequest.getHeader("Authorization");
if (token != null && !token.isEmpty()) {
AuthenticationTokenRepo authenticationTokenRepository = WebApplicationContextUtils
.getRequiredWebApplicationContext(httpRequest.getServletContext())
.getBean(AuthenticationTokenRepo.class);
// check whether token is valid
AuthenticationToken authToken = authenticationTokenRepository.findByToken(token);
if (authToken != null) {
// Add user to SecurityContextHolder
final UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(
authToken.getUser(), null, new ApplicationUserDetail(authToken.getUser()).getAuthorities());
SecurityContextHolder.getContext().setAuthentication(authentication);
}
}
chain.doFilter(request, response);
SecurityContextHolder.clearContext();
}
SecurityConfig.java
#Configuration
#EnableWebSecurity
#EnableGlobalMethodSecurity(securedEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
#Autowired
private Environment environment;
#Override
protected void configure(final HttpSecurity http) throws Exception {
http.csrf().disable().sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
.authorizeRequests().antMatchers(HttpMethod.POST, "/api/v1/login").permitAll()
.antMatchers(HttpMethod.POST, "/api/v1/user/register").permitAll().anyRequest().authenticated();
// Implementing Token based authentication in this filter
final TokenAuthenticationFilter tokenFilter = new TokenAuthenticationFilter();
http.addFilterBefore(tokenFilter, BasicAuthenticationFilter.class);
}
#Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers(UrlConstant.BASE_ADMIN_URI_V1 + UrlConstant.CURRENCY_LIST_FOR_MARKET);
web.ignoring().antMatchers(UrlConstant.BASE_ADMIN_URI_V1 + UrlConstant.TRADING_FEES);
}
}
}
ApplicationUserDetail.java
public class ApplicationUserDetail implements UserDetails,Serializable {
private static final long serialVersionUID = 1L;
transient User user;
public ApplicationUserDetail(User user) {
this.user = user;
}
#Override
public Collection<? extends GrantedAuthority> getAuthorities() {
AuthorityUtils.commaSeparatedStringToAuthorityList(user.getRole().getName());
}
#Override
public String getPassword() {
return user.getPassword();
}
#Override
public String getUsername() {
return user.getEmailId();
}
#Override
public boolean isAccountNonExpired() {
return true;
}
#Override
public boolean isAccountNonLocked() {
return true;
}
#Override
public boolean isCredentialsNonExpired() {
return true;
}
#Override
public boolean isEnabled() {
return user.getIsEnabled();
}
}
More info: When i am printing response in my api it is correct but when i am printing it in my Authentication filter i got the response of fee api in my currency api so i think there is some problem between api to filter.