I'm trying to manually authenticate to Spring Security through REST API. I've managed to open an endpoint where users can perfom the authentication. The endpoint works only whithin itself. If I try to get data from other secured endpoints the authentication is not provided. It seems that the user is authenticated but not remembered. Am I missing some steps?
Here's my endpoint where users can login:
#RestController
#RequestMapping("/api/account/login")
public class LoginRestController {
#Autowired
AuthenticationManager authenticationManager;
#PostMapping("")
public ResponseEntity<String> login(#RequestBody LoginPayload payload) {
try {
Authentication auth = authenticationManager
.authenticate(new UsernamePasswordAuthenticationToken(payload.getEmail(), payload.getPassword()));
SecurityContextHolder.getContext().setAuthentication(auth);
return new ResponseEntity<>("Utente autenticato", HttpStatus.OK);
} catch (Exception e) {
SecurityContextHolder.getContext().setAuthentication(null);
return new ResponseEntity<>(e.getMessage(), HttpStatus.UNAUTHORIZED);
}
}
}
here's my configuration:
#Configuration
public class SecurityConfig {
#Bean
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.csrf().disable()
.authorizeHttpRequests((requests) -> requests
.requestMatchers("/api/account/register", "/api/account/register/**").permitAll()
.requestMatchers("/api/account/login").permitAll()
.requestMatchers("/api/aziende/exists").permitAll()
.requestMatchers("/api", "/api/**").authenticated()
.requestMatchers("/app", "/app/**").authenticated()
.requestMatchers("/", "/**").permitAll()
)
.formLogin((form) -> form.loginPage("/accedi").permitAll())
.logout((logout) -> logout.logoutUrl("/esci").permitAll());
return http.build();
}
#Bean
PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
#Bean
AuthenticationManager authenticationManager(
AuthenticationConfiguration configuration) throws Exception {
return configuration.getAuthenticationManager();
}
}
UPDATED:
As #MarcusHertdaCoregio said I had to save the context with the securitycontextrepository. Here's the code:
#Configuration
#EnableWebSecurity
public class SecurityConfig {
#Bean
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http = http.csrf().disable();
http = http.authorizeHttpRequests((requests) -> requests
.requestMatchers("/api/auth/**").permitAll()
.requestMatchers("/api/aziende/exists").permitAll()
.requestMatchers("/api", "/api/**").authenticated()
.requestMatchers("/app", "/app/**").authenticated()
.requestMatchers("/admin", "/admin/**").hasAnyAuthority("ROLE_ADMIN")
.requestMatchers("/", "/**").permitAll()
);
http = http.formLogin((form) -> form.loginPage("/accedi").permitAll());
http = http.logout((logout) -> logout.logoutUrl("/esci").logoutSuccessUrl("/").permitAll());
http = http.securityContext((securityContext) -> securityContext
.requireExplicitSave(true));
return http.build();
}
#Bean
PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
#Bean
AuthenticationManager authenticationManager(
AuthenticationConfiguration configuration) throws Exception {
return configuration.getAuthenticationManager();
}
#Bean
SecurityContextRepository securityContextRepository() {
return new HttpSessionSecurityContextRepository();
}
}
#PostMapping("/login")
public ResponseEntity<?> login(#RequestBody Login payload, HttpServletRequest request,
HttpServletResponse response) {
SecurityContext context = SecurityContextHolder.createEmptyContext();
try {
UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(payload.getEmail(),
payload.getPassword());
Authentication auth = authManager.authenticate(token);
if (auth.isAuthenticated()) {
context.setAuthentication(auth);
SecurityContextHolder.setContext(context);
securityContextRepository.saveContext(context, request, response);
return ResponseEntity.ok("utente autenticato");
} else {
return ResponseEntity.status(401).body("credenziali non valide");
}
} catch (Exception e) {
return ResponseEntity.status(401).body(e.getMessage());
}
}
Related
I have a problem with cycle injection and don't know how to fix it :(
For my SecurityConfig I need JWTAuthenticationFilter.
For my JWTAuthenticationFilter I need AuthenticationManager.
For my AuthenticationManager I need SecurityConfig.
Can you help me with resolving this problem ? Thanks !
#Configuration
#RequiredArgsConstructor
public class ApplicationConfig {
private final UserRepository userRepository;
#Bean
public UserDetailsService userDetailsService() {
return username -> userRepository.findByEmail(username)
.orElseThrow(() -> new UsernameNotFoundException("User not found"));
}
#Bean
public AuthenticationProvider authenticationProvider() {
DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider();
authProvider.setUserDetailsService(userDetailsService());
authProvider.setPasswordEncoder(passwordEncoder());
return authProvider;
}
#Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
#Bean
public AuthenticationManager authenticationManager(AuthenticationManagerBuilder auth ) throws Exception {
return auth.userDetailsService(userDetailsService())
.passwordEncoder(passwordEncoder()).and().build();
}
#Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception {
return config.getAuthenticationManager();
}
}
#Configuration
#EnableWebSecurity
#RequiredArgsConstructor
public class SecurityConfig {
private final JWTVerifierFilter jwtVerifierFilter;
private final AuthenticationProvider authenticationProvider;
private final JWTAuthenticationFilter jwtAuthenticationFilter;
#Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.csrf()
.disable()
.authorizeHttpRequests()
.requestMatchers("/auth/**")
.permitAll()
.anyRequest()
.authenticated()
.and()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authenticationProvider(authenticationProvider)
.addFilter(jwtAuthenticationFilter)
.addFilterAfter(jwtVerifierFilter, JWTAuthenticationFilter.class);
return http.build();
}
}
#RequiredArgsConstructor
#Component
public class JWTAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
private final AuthenticationManager authenticationManager;
private final TokenRepository tokenRepository;
private final JwtService jwtService;
private final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
#Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
try {
JwtAuthenticationRequest authModel = OBJECT_MAPPER.readValue(request.getInputStream(), JwtAuthenticationRequest.class);
Authentication authentication = new UsernamePasswordAuthenticationToken(authModel.getUsername(), authModel.getPassword());
return authenticationManager.authenticate(authentication);
} catch (IOException e) {
throw new AssertionError(e.getMessage(), e);
}
}
#Override
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException {
String token = jwtService.generateToken((UserEntity) authResult.getPrincipal());
TokenEntity tokenEntity = TokenEntity.builder()
.id(UUID.randomUUID().toString())
.authenticationToken(token)
.username(authResult.getName())
.build();
tokenEntity = tokenRepository.save(tokenEntity);
response.addHeader(HttpHeaders.AUTHORIZATION, String.format("Bearer %s", tokenEntity.getId()));
ConnValidationResponse respModel = ConnValidationResponse.builder().isAuthenticated(true).build();
response.addHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE);
response.getOutputStream().write(OBJECT_MAPPER.writeValueAsBytes(respModel));
}
}
I tried to replace
#Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception {
return config.getAuthenticationManager();
}
by
#Bean
public AuthenticationManager authenticationManagerBean(HttpSecurity http) throws Exception {
AuthenticationManagerBuilder authenticationManagerBuilder = http.getSharedObject(AuthenticationManagerBuilder.class);
authenticationManagerBuilder.userDetailsService(jwtUserDetailsService).passwordEncoder(passwordEncoder());
return authenticationManagerBuilder.build();
}
but for HttpSecurity I need SecurityConfig too.
I also tried add #Lazy for AuthenticationConfiguration config but it doesn't work
I have a Spring boot application, and I use Keycloak for security. Logging in and out works, but I'd like to have the confirmation dialog from Keycloak like this when the logout button is clicked. How can I get that? Right now, it logs out directly.
SecurityConfig class:
#Configuration
#EnableWebSecurity
public class SecurityConfigKeycloak {
private final KeycloakLogoutHandler keycloakLogoutHandler;
public SecurityConfigKeycloak(KeycloakLogoutHandler keycloakLogoutHandler) {
this.keycloakLogoutHandler = keycloakLogoutHandler;
}
#Bean
protected SessionAuthenticationStrategy sessionAuthenticationStrategy() {
return new RegisterSessionAuthenticationStrategy(new SessionRegistryImpl());
}
#Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/someurl/**", "/anotherurl/**").permitAll()
.anyRequest()
.fullyAuthenticated();
http.oauth2Login()
.and()
.logout()
.addLogoutHandler(keycloakLogoutHandler)
.logoutSuccessUrl("/");
return http.build();
}
}
Logout Handler:
#Slf4j
#Component
public class KeycloakLogoutHandler implements LogoutHandler {
private final RestTemplate restTemplate;
public KeycloakLogoutHandler(RestTemplateBuilder builder) {
this.restTemplate = builder.build();
}
#Override
public void logout(HttpServletRequest request, HttpServletResponse response, Authentication auth) {
logoutFromKeycloak((OidcUser) auth.getPrincipal());
}
private void logoutFromKeycloak(OidcUser user) {
log.debug("Entering logout...");
String endSessionEndpoint = user.getIssuer() + "/protocol/openid-connect/logout";
UriComponentsBuilder builder = UriComponentsBuilder
.fromUriString(endSessionEndpoint)
.queryParam("id_token_hint", user.getIdToken().getTokenValue());
ResponseEntity<String> logoutResponse = restTemplate.getForEntity(builder.toUriString(), String.class);
if (logoutResponse.getStatusCode().is2xxSuccessful()) {
log.info("Successfulley logged out from Keycloak");
} else {
log.error("Could not propagate logout to Keycloak");
}
}
}
Controller class:
#Slf4j
#RestController
#RequestMapping("/logout")
public class LogoutController {
#GetMapping(value = "/*")
public ResponseEntity logout(HttpServletRequest request) {
try {
request.logout();
return new ResponseEntity<>(HttpStatus.OK);
} catch (ServletException e) {
log.error(e.getMessage(), e);
return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR);
}
}
}
I am trying to implement JWT security. Somewhere, service is being called before controller with null value #RequestBody.
Below are the codes.
WebSecurityConfig.java
protected void configure(HttpSecurity http) throws Exception
{
http
.csrf().disable()
.authorizeRequests()
.antMatchers("/authenticate")
.permitAll()
.anyRequest()
.authenticated()
.and()
.httpBasic();
}
#Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(jwtUserDetailsService).passwordEncoder(passwordEncoder());
}
#Bean
public PasswordEncoder passwordEncoder(){
return NoOpPasswordEncoder.getInstance();
}
#Bean
#Override
public AuthenticationManager authenticationManagerBean() throws Exception{
return super.authenticationManagerBean();
}
#Service
public class JWTUserDetailsService implements UserDetailsService {
#Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
if("jailhouse".equals(username))
return new User("jailhouse","jailhouse",new ArrayList<>());
else {
throw new BadCredentialsException("User not found with Username: "+username);
}
}
}
#Controller
#PostMapping("/authenticate")
public JWTResponse authenticate(#RequestBody JWTRequest jwtRequest)throws Exception{
try{
authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(jwtRequest.getUsername(),jwtRequest.getPassword())
);
}catch (BadCredentialsException e){
throw new BadCredentialsException("INVALID_CREDENTIAL",e);
}
final UserDetails userDetails = jwtUserDetailsService.loadUserByUsername(jwtRequest.getUsername());
final String token = jwtUtil.generateToken(userDetails);
return new JWTResponse(token);
}
Everytime, I hit on authenticate endpoint somehow, service is being executed first with null value.
.security.authentication.BadCredentialsException: User not found with Username: NONE_PROVIDED
at com.fiserv.dc.service.JwtUserDetailsService.loadUserByUsername(JwtUserDetailsService.java:18) ~[main/:na]
I am using Spring Security and using Username Password Authentication Filter. I want to know if it is possible to show the authentication endpoint in Swagger. This endpoint is generated automatically by the filter (as far I understood).
I really want to make this endpoint appear in Swagger-ui, otherwise I will need to login in Postman and then use Swagger with the Jwt Token, which is little strange.
This is the UsernamePasswordAuthenticationFilter:
public class AuthenticationFilter extends UsernamePasswordAuthenticationFilter {
private final AuthenticationManager authenticationManager;
public AuthenticationFilter(AuthenticationManager authenticationManager) {
this.authenticationManager = authenticationManager;
}
#Override
public Authentication attemptAuthentication(HttpServletRequest req,
HttpServletResponse res) {
try {
LoginDTO userEntity = new ObjectMapper().readValue(req.getInputStream(), LoginDTO.class);
return authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(userEntity.getEmail(),
userEntity.getPassword(), new ArrayList<>())
);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
#Override
protected void successfulAuthentication(HttpServletRequest req, HttpServletResponse res, FilterChain chain,
Authentication auth) throws IOException {
Token token = new Token(JwtUtils.createAccessToken(ZonedDateTime.now().plusMinutes(10), TenantContext.getCurrentUserUniqueIdentifier(), TenantContext.getCurrentTenantId()),
JwtUtils.createRefreshToken(ZonedDateTime.now().plusMonths(3)));
PrintWriter out = res.getWriter();
res.setContentType("application/json");
res.setCharacterEncoding("UTF-8");
out.print(new ObjectMapper().writeValueAsString(token));
out.flush();
}
}
This is my class extending from WebSecurityAdapter. As you can see, I set the url as /v1/login.
#EnableWebSecurity
#AllArgsConstructor
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
private final UserEntityDetailsService userEntityDetailsService;
private final BCryptPasswordEncoder bCryptPasswordEncoder;
#Override
protected void configure(HttpSecurity http) throws Exception {
http.cors().and().csrf().disable().authorizeRequests()
.antMatchers(HttpMethod.POST, "/v1/account").permitAll()
.antMatchers(HttpMethod.GET, "/v1/healthcheck").permitAll()
.antMatchers("/v1/recoverpassword/**").permitAll()
.antMatchers("/swagger-ui/**", "/v2/api-docs", "/configuration/ui", "/swagger-resources/**", "/configuration/**", "/swagger-ui.html", "/webjars/**").permitAll()
.anyRequest().authenticated()
.and()
.addFilter(getAuthenticationFilter())
.addFilter(new AuthorizationFilter(authenticationManager()))
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
}
#Bean
CorsConfigurationSource corsConfigurationSource() {
final UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", new CorsConfiguration().applyPermitDefaultValues());
return source;
}
#Override
public void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userEntityDetailsService).passwordEncoder(bCryptPasswordEncoder);
}
public AuthenticationFilter getAuthenticationFilter() throws Exception {
final AuthenticationFilter filter = new AuthenticationFilter(authenticationManager());
filter.setFilterProcessesUrl("/v1/login");
return filter;
}
}
This is my SpringFox config:
#Configuration
#EnableSwagger2
public class SpringFoxConfig {
private ApiInfo apiInfo() {
return new ApiInfoBuilder()
.title("Projeto List")
.description("All endpoints of Projeto List Api")
.build();
}
#Bean
public Docket api() {
return new Docket(DocumentationType.SWAGGER_2)
.useDefaultResponseMessages(false)
.apiInfo(apiInfo())
.select()
.apis(RequestHandlerSelectors.basePackage("br.com.projetolist.resource"))
.paths(PathSelectors.ant("/**"))
.build()
.securitySchemes(Arrays.asList(apiKey()));
}
/**
* SwaggerUI information
*/
#Bean
UiConfiguration uiConfig() {
return UiConfigurationBuilder.builder()
.deepLinking(true)
.displayOperationId(false)
.defaultModelsExpandDepth(1)
.defaultModelExpandDepth(1)
.defaultModelRendering(ModelRendering.MODEL)
.displayRequestDuration(false)
.docExpansion(DocExpansion.NONE)
.filter(false)
.maxDisplayedTags(null)
.operationsSorter(OperationsSorter.ALPHA)
.showExtensions(false)
.tagsSorter(TagsSorter.ALPHA)
.validatorUrl(null)
.build();
}
private ApiKey apiKey() {
return new ApiKey("jwtToken", "Authorization", "header");
}
}
You can just add a similar method to one of your controllers. In your case, it will be the POST method with URL "/v1/login".
#PostMapping("/v1/login")
#ResponseStatus(OK)
fun login(#RequestBody userCreds: UserCredentials) {}
I have created a JWT authentication filter for my Spring Rest backend. Creating a JWT doesn't appear to be an issue, however with my current setup, any request is authenticated, no request triggers a 401 despite the fact that the client is not passing any tokens in the header.
My WebSecurityConfig:
#Configuration
#EnableWebSecurity
#EnableGlobalMethodSecurity(securedEnabled = true,
jsr250Enabled = true, prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
private JwtAuthenticationEntryPoint unauthorizedHandler;
private CustomUserDetailsService customUserDetailsService;
#Autowired
public WebSecurityConfig(final JwtAuthenticationEntryPoint unauthorizedHandler,
final CustomUserDetailsService customUserDetailsService) {
this.unauthorizedHandler = unauthorizedHandler;
this.customUserDetailsService = customUserDetailsService;
}
#Bean
public JwtAuthenticationFilter jwtAuthenticationFilter() {
return new JwtAuthenticationFilter();
}
#Bean
public JwtAuthenticationSuccessHandler jwtAuthenticationSuccessHandler() {
return new JwtAuthenticationSuccessHandler();
}
#Override
public void configure(AuthenticationManagerBuilder authenticationManagerBuilder) throws Exception {
authenticationManagerBuilder
.userDetailsService(customUserDetailsService)
.passwordEncoder(passwordEncoder());
}
#Bean(BeanIds.AUTHENTICATION_MANAGER)
#Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
#Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
/**
* {#inheritDoc}
*/
#Override
protected void configure(final HttpSecurity http) throws Exception {
http
.csrf()
.disable()
.cors()
.and()
.exceptionHandling()
.authenticationEntryPoint(unauthorizedHandler)
.and()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.antMatcher("/api")
.authorizeRequests()
.anyRequest()
.authenticated()
.and()
.addFilterBefore(jwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
}
/**
* Sets security evaluation context.
*
* #return {#link SecurityEvaluationContextExtension}
*/
#Bean
public SecurityEvaluationContextExtension securityEvaluationContextExtension() {
return new SecurityEvaluationContextExtension();
}
}
I have set this up such that all requests require authorisation. My JwtAuthenticationEntryPoint is as expected: a generic 401 error being thrown.
My JwtAuthenticationFilter:
#Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {
#Autowired
private JwtTokenProvider tokenProvider;
#Autowired
private CustomUserDetailsService customUserDetailsService;
private static final Logger logger = LoggerFactory.getLogger(JwtAuthenticationFilter.class);
#Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain
filterChain) throws ServletException, IOException {
logger.debug("Filtering request for JWT header verification");
try {
String jwt = getJwtFromRequest(request);
if (StringUtils.hasText(jwt) && tokenProvider.validateToken(jwt)) {
String username = tokenProvider.getUserIdFromJWT(jwt);
UserDetails userDetails = customUserDetailsService.loadUserByUsername(username);
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken
(userDetails, null, userDetails.getAuthorities());
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authentication);
}
} catch (Exception ex) {
logger.error("Could not set user authentication in security context", ex);
}
filterChain.doFilter(request, response);
}
private String getJwtFromRequest(HttpServletRequest request) {
logger.debug("Attempting to get token from request header");
String bearerToken = request.getHeader("Authorization");
if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) {
return bearerToken.substring(7, bearerToken.length());
}
return null;
}
}
Found the problem.
I had to include a reference to the filter in my web.xml file, this isn't picked up automatically using component scanner.
something like:
<filter>
<filter-name>jwtFilter</filter-name>
<filter-class>com.path.to.JwtFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>jwtFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>