I understand this question has been asked before, but none of what worked for the others worked for me. That said, here it is:
After setting up .logout() and its relevant associate methods, I still cannot make my Spring server logout properly. To be precise, this is the sequence I'm following:
POST login, get token
Use token for other HTTP requests, such as a standard GET for DB data
Logout using the vanilla logout URL
Try to do step 2 again, to see if logout was successful.
At 4, for a reason that is unknown to me, I'm still able to GET the data, which is wrong.
Some of the things I've tried are:
.clearAuthentication(true).invalidateHttpSession(true)
Setting up the following bean on my SecurityConfig:
#Bean
public static ServletListenerRegistrationBean httpSessionEventPublisher() {
return new ServletListenerRegistrationBean(new HttpSessionEventPublisher());
}
None of it worked. Below are my security configurations. Any clues and help is much appreciated, and I thank you in advance.
SecurityConfig.java
#Configuration
public class SecurityConfig {
#Autowired
private AuthenticationManager authenticationManager;
private final AuthSuccessHandler authSuccessHandler;
private final JwtUserDetailsService jwtUserDetailsService;
private final String secret;
SecurityConfig(AuthSuccessHandler authSuccessHandler, JwtUserDetailsService jwtUserDetailsService, #Value("${jwt.secret}") String secret) {
this.authSuccessHandler = authSuccessHandler;
this.jwtUserDetailsService = jwtUserDetailsService;
this.secret = secret;
}
#Bean
public static ServletListenerRegistrationBean httpSessionEventPublisher() {
return new ServletListenerRegistrationBean(new HttpSessionEventPublisher());
}
#Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.cors()
.and()
.csrf()
.disable()
.authorizeHttpRequests((auth) -> {
try {
auth
.requestMatchers("/user").hasRole("USER")
.requestMatchers("/admin").hasRole("ADMIN")
.requestMatchers("**/encoding-results/**", "**/encoding-results").hasAnyRole("ADMIN")
.requestMatchers("**/video/**").hasAnyRole("ADMIN")
.requestMatchers("**/codec/**").hasAnyRole("ADMIN")
.requestMatchers("**/auth/**").permitAll()
.anyRequest().authenticated()
.and()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.addFilter(authenticationFilter())
.addFilter(new JwtAuthorizationFilter(authenticationManager, jwtUserDetailsService, secret))
.logout(logout -> logout
.clearAuthentication(true)
.logoutUrl("/logout")
.invalidateHttpSession(true)
.deleteCookies("JSESSIONID"))
.exceptionHandling()
.authenticationEntryPoint(new HttpStatusEntryPoint(HttpStatus.UNAUTHORIZED));
} catch (Exception e) {
throw new RuntimeException(e);
}
})
.httpBasic(Customizer.withDefaults());
return http.build();
}
#Bean
public JsonObjectAuthenticationFilter authenticationFilter() throws Exception {
JsonObjectAuthenticationFilter filter = new JsonObjectAuthenticationFilter();
filter.setAuthenticationSuccessHandler(authSuccessHandler);
filter.setAuthenticationManager(authenticationManager);
return filter;
}
}
You cant logout a JWT, a JWT is valid as long as you have set the validation time to be.
OWASP has written about this problem No Built-In Token Revocation by the User
You have written a custom login that issues tokens. The server has no idea how many tokens that have been issued and which tokens that are logged in or not.
So no you can't logout the user, unless you keep some form of state of the issued tokens on the server side, as in storing the tokens in a database and invalidating them when someone does a logout.
And ofc deleting the sessionid cookie wont do anything since you are not using session cookies.
This is the exact reason why you shouldn't be using JWTs as session replacements.
Stop using JWT for sessions
Stop using JWT for sessions, part 2
JSON Web Tokens (JWT) are Dangerous for User Sessions
Why JWTs Suck as Session Tokens
Related
I have this error related to Spring security that I cannot fix, and it is happening only though the browser. When I try to replicate it through postman, it suddenly works and everything's fine.
I authorized unauthenticated requests for /login, and authenticated requests to the rest of the API.
Therefore when I make a call to /login, it does let me authenticate.
But, when I make a call to any of my API endpoints that is not /login, and I'm already authenticated, my backend returns this strange error:
I don't really know what this error means.
However, when I do the same process through Postman, it works and the endpoint returns what it is supposed to return.
Login API call:
Another API call that is not login:
As you can see, through Postman it does work well.
But doing the exact same through the Angular UI I'm developing leads to the former error.
My Spring security configuration:
#Configuration
#AllArgsConstructor
#EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
private static final String FRONTEND_URL = "http://localhost:4200/";
private final UserService userService;
private final BCryptPasswordEncoder bCryptPasswordEncoder;
#Override
protected void configure(HttpSecurity http) throws Exception {
http.cors()
.and()
.csrf().disable()
.authorizeRequests()
.antMatchers("/api/v*/registration/**", "/login-status/**")
.permitAll()
.anyRequest().authenticated()
.and()
.formLogin()
.permitAll()
.loginPage(FRONTEND_URL + "/login")
.loginProcessingUrl("/login")
.defaultSuccessUrl("/login-status/success", true)
.failureUrl("/login-status/failure")
.and()
.logout()
.permitAll();
}
#Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.authenticationProvider(daoAuthenticationProvider());
}
#Bean
public DaoAuthenticationProvider daoAuthenticationProvider() {
DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
provider.setPasswordEncoder(bCryptPasswordEncoder);
provider.setUserDetailsService(userService);
return provider;
}
#Bean
CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedOrigins(Arrays.asList(FRONTEND_URL));
configuration.setAllowedMethods(Arrays.asList("GET", "POST"));
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
return source;
}
}
I hope you could help me cause I'm really stuck with this. I'm a front-end dev who is trying to find my ways to develop a backend, just to learn, but man, shits hard. Thank you very much, and I hope my bad English didn't hurt your eyes too much. Thank you for your help.
We have a simple application with only two consumers and 5 endpoints. For one endpoint I need some way of authentication. I like the stripe way of doing this, but I don't know how I can build this in spring boot.
"Authentication to the API is performed via HTTP Basic Auth. Provide your API key as the basic auth username value. You do not need to provide a password."
#Configuration
#EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/qr")
.hasRole("user")
.and()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.httpBasic()
.and()
.csrf()
.disable();
}
#Bean
public UserDetailsService userDetailsService() {
val encodedPassword = new BCryptPasswordEncoder().encode("test");
final InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
manager.createUser(User.withUsername("admin").password(encodedPassword).roles("user").build());
//manager.createUser(User.withUsername("admin").roles("user").build());
return manager;
}
#Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
}
I tried to remove the password from manager.createUser but this doesn't work.
Basic authentication is made of user:password in base64 encoded form.
So your user must have a password equal to empty string for Basic Authentication to work.
You can also get rid of BCryptPasswordEncoder and use NoOpPasswordEncoder since you don't use the password value.
Preconditions
I have two Java Spring applications(App 'A' and App 'B') that were created via JHipster(monolithic application). Both applications uses keycloak for authentication/authorization.
Both applications have an angular frontend and support login via ouath (spring-security). Here ist my SecurityConfiguration of Application A and B:
#Configuration
#Import(SecurityProblemSupport.class)
#EnableOAuth2Sso
#EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
private final CorsFilter corsFilter;
private final SecurityProblemSupport problemSupport;
public SecurityConfiguration(CorsFilter corsFilter, SecurityProblemSupport problemSupport) {
this.corsFilter = corsFilter;
this.problemSupport = problemSupport;
}
#Bean
public AjaxLogoutSuccessHandler ajaxLogoutSuccessHandler() {
return new AjaxLogoutSuccessHandler();
}
#Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
#Bean
public SecurityEvaluationContextExtension securityEvaluationContextExtension() {
return new SecurityEvaluationContextExtension();
}
#Override
public void configure(WebSecurity web) throws Exception {
web.ignoring()
.antMatchers(HttpMethod.OPTIONS, "/**")
.antMatchers("/app/**/*.{js,html}")
.antMatchers("/i18n/**")
.antMatchers("/content/**")
.antMatchers("/swagger-ui/index.html")
.antMatchers("/test/**")
.antMatchers("/h2-console/**");
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf()
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
.and()
.addFilterBefore(corsFilter, CsrfFilter.class)
.exceptionHandling()
.authenticationEntryPoint(problemSupport)
.accessDeniedHandler(problemSupport)
.and()
.logout()
.logoutUrl("/api/logout")
.logoutSuccessHandler(ajaxLogoutSuccessHandler())
.permitAll()
.and()
.headers()
.frameOptions()
.disable()
.and()
.authorizeRequests()
.antMatchers("/api/profile-info").permitAll()
.antMatchers("/api/**").authenticated()
.antMatchers("/management/health").permitAll()
.antMatchers("/management/**").hasAuthority(AuthoritiesConstants.ADMIN)
.antMatchers("/v2/api-docs/**").permitAll()
.antMatchers("/swagger-resources/configuration/ui").permitAll()
.antMatchers("/swagger-ui/index.html").hasAuthority(AuthoritiesConstants.ADMIN);
}
}
In App B i also have an ResourceServerConfiguration. This checks if the header contains an "Authorization" key. If true, the user can login via JWT(Bearer Authentication). I tested this via Postman and it works fine:
#Configuration
#EnableResourceServer
public class ResourceServerConfiguration extends ResourceServerConfigurerAdapter {
#Override
public void configure(final HttpSecurity http) throws Exception {
http
.requestMatcher(new RequestHeaderRequestMatcher("Authorization")).authorizeRequests()
.anyRequest().authenticated();
}
}
Further more both apps are in the same keycloak realm and have the access-type "public".
Problem:
Now i want to call an endpoint of App B via an Spring RestTemplate from App A. The problem is, that i do not have an access_token that i can put in my rest request/restTemplate. When i look in my request that is send from my frontend, i only got an JSESSIONID. There is no access_token/JWT in the header.
Question
Is there a way to get the access_token of the current user out of the JSESSIONID/the HttpSession or the spring security context? Do i need something like a Tokenstore where i store every token that comes from keycloak?
Did anyone else have similar problems or any idea how i could solve that problem?
After some research it turns out that the problem lies within the generated jhipster code.
I followed the authentication process in the application and saw, that there was a call to the /account endpoint directly after authentication, where the user information were retrieved. The call is triggerd by the frontend. First time this endpoint is called, there is a principal with a bearer token available. Within the /account endpoint, a call to the userService with the principal object is performed. More precisley
getUserFromAuthentication(OAuth2Authentication authentication)
is called. Within this method there is a part that replaces the OAuth2Authentication with a new UsernamePasswordAuthenticationToken and inserts it into the SecurityContext:
UsernamePasswordAuthenticationToken token = getToken(details, user,
grantedAuthorities);
authentication = new OAuth2Authentication(authentication.getOAuth2Request(), token);
SecurityContextHolder.getContext().setAuthentication(authentication);
So after that, the access_token is lost. I am not quite sure, why it was replaced with the new OAuth2Authentication, but i tend to extend this part and keep the access_token in my securityContext for further restcalls.
I need to configure expired-url in my Spring MVC application. Here is my effort, but has no effect:
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.addFilterBefore(adminAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class)
.addFilterBefore(customerAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class)
.csrf()
.disable()
.authorizeRequests()
.antMatchers("...", "...", "...").permitAll()
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/admin/login")
.and()
.logout()
.addLogoutHandler(customLogoutHandler())
.logoutSuccessHandler(customLogoutSuccessHandler())
.logoutUrl("/logout")
.deleteCookies("remove")
.invalidateHttpSession(true)
.permitAll()
.and()
.sessionManagement()
.maximumSessions(1)
.expiredUrl("/expired");
}
This does not have any effect and when the user's session times out, spring does not redirect him to /expired url and just redirects him to /admin/login url.
Update:
I tried suggested solutions in the comments and answer, but did not see any effect. Also I removed addLogoutHandler(), logoutSuccessHandler() and two addFilterBefore() at beginning of method, but not working.
Also I tried another solution in this way:
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.addFilterBefore(sessionManagementFilter(), SessionManagementFilter.class)
.csrf()
.disable()
.authorizeRequests()
.antMatchers("...", "...", "...").permitAll()
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/admin/login")
.and()
.logout()
.logoutUrl("/logout")
.deleteCookies("remove")
.invalidateHttpSession(true)
.permitAll();
}
#Bean
public SessionManagementFilter sessionManagementFilter() {
SessionManagementFilter sessionManagementFilter = new SessionManagementFilter(httpSessionSecurityContextRepository());
sessionManagementFilter.setInvalidSessionStrategy(simpleRedirectInvalidSessionStrategy());
return sessionManagementFilter;
}
#Bean
public SimpleRedirectInvalidSessionStrategy simpleRedirectInvalidSessionStrategy() {
SimpleRedirectInvalidSessionStrategy simpleRedirectInvalidSessionStrategy = new SimpleRedirectInvalidSessionStrategy("/expired");
return simpleRedirectInvalidSessionStrategy;
}
#Bean
public HttpSessionSecurityContextRepository httpSessionSecurityContextRepository(){
HttpSessionSecurityContextRepository httpSessionSecurityContextRepository = new HttpSessionSecurityContextRepository();
return httpSessionSecurityContextRepository;
}
Could anyone help me to solve this problem?
ConcurrentSessionFilter will redirect to expiredUrl, if the valid session ID is marked as expired in SessionRegistry, see Spring Security reference:
- expired-url The URL a user will be redirected to if they attempt to use a session which has been "expired" by the concurrent session controller because the user has exceeded the number of allowed sessions and has logged in again elsewhere. Should be set unless exception-if-maximum-exceeded is set. If no value is supplied, an expiry message will just be written directly back to the response.
SessionManagementFilter will redirect to invalidSessionUrl, if the session ID is not valid (timeout or wrong ID), see Spring Security reference:
If the user is not currently authenticated, the filter will check whether an invalid session ID has been requested (because of a timeout, for example) and will invoke the configured InvalidSessionStrategy, if one is set. The most common behaviour is just to redirect to a fixed URL and this is encapsulated in the standard implementation SimpleRedirectInvalidSessionStrategy. The latter is also used when configuring an invalid session URL through the namespace,as described earlier.
Both URLs (expiredUrl, invalidSessionUrl) have to be configured as permitAll().
BTW: If you want to use Concurrent Session Control with maximumSessions you have to add HttpSessionEventPublisher to your web.xml:
Concurrent Session Control
If you wish to place constraints on a single user’s ability to log in to your application, Spring Security supports this out of the box with the following simple additions. First you need to add the following listener to your web.xml file to keep Spring Security updated about session lifecycle events:
<listener>
<listener-class>
org.springframework.security.web.session.HttpSessionEventPublisher
</listener-class>
</listener>
I tried the Ali Dehghani's solution(in the comments) in this way:
.sessionManagement().maximumSessions(1).and().invalidSessionUrl("/expired");
And as The Coder said, add "/expired" in the permitted urls and the problem solved. Thank you everybody who has paid attention to my problem, especially Ali Dehghani and The Coder, for their useful comments.
Ideally your UX should simply redirect your user back to the login page. I guess you see the requirement of having a dedicated /expired page because of Spring MVC - change security settings dynamically where you informed about your need of having separate login masks. If the workaround (the one that I described in my answer on your other question) works for you, you could maybe drop your requirement of having a dedicated /expired page and redirect the user to the correct login page directly using teh solution approach number (2). How about that?
Nevertheless, to answer your current question...
I'm not sure if it works but give it a try and change your code
//...
.sessionManagement()
.maximumSessions(1)
.expiredUrl("/expired");
}
to
//...
.sessionManagement().sessionFixation().newSession().maximumSessions(1)
.expiredUrl("/expired")
.sessionRegistry(sessionRegistry());
}
#Bean
public SessionRegistry sessionRegistry() {
SessionRegistry sessionRegistry = new SessionRegistryImpl();
return sessionRegistry;
}
In case it doesn't work, could you then post the code of your customLogoutHandler() and customLogoutSuccessHandler()? You are using Spring MVC outside of Spring Boot, correct?
If you use UserDetails and UserDetailsService then it should be because your UserDetails implementation class there is no Override hashCode () and equals (Object obj) method. This is my implementation class for UserDetails:
public class MyUser implements UserDetails {
private String username;
private String password;
public void setUsername(String username) {
this.username = username;
}
public void setPassword(String password) {
this.password = password;
}
#Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return null;
}
#Override
public String getPassword() {
return null;
}
#Override
public String getUsername() {
return null;
}
#Override
public boolean isAccountNonExpired() {
return false;
}
#Override
public boolean isAccountNonLocked() {
return false;
}
#Override
public boolean isCredentialsNonExpired() {
return false;
}
#Override
public boolean isEnabled() {
return false;
}
#Override
public int hashCode() {
return username.hashCode();
}
#Override
public boolean equals(Object obj) {
return this.toString().equals(obj.toString());
}
}
I'm developing a web application unsing Spring Security 4.0.0's Java Config instead of xml config. I'm using ObjectPostProcessors to customize the some of Spring Security's beans, notably the session consurrency ones (to achive immediate invalidation of a session as soon as a user logs in again, as opposed to Spring's standard behavior of invalidating at the next request).
It's working as expected most of the times, but sometimes when I restart the application it seems like not all beans get modified as I want.
Are SecurityBuilders processed in a specific order or are they instead processed with a ramdom order?
EDIT:
My Config
#EnableWebSecurity
public class SecurityConfig extends AbstractCASWebSecurityConfigurerAdapter {
public SecurityConfig() {
super(true, false, true);
}
#Autowired
private Environment env;
// we need a custom SessionRegistry as there's no way to get ahold of the one created by the configurer.
#Bean
public SessionRegistry sessionRegistry() {
return new SessionRegistryImpl();
}
// we need a custom HttpSessionCsrfTokenRepository as there's no way to get ahold of the one created by the configurer.
#Bean
public CsrfTokenRepository csrfTokenRepository() {
return new HttpSessionCsrfTokenRepository();
}
// our custom ConcurrentSessionControlAuthenticationStrategy that invalidates session immediately
#Bean
public SessionInvalidatingConcurrentSessionControlAuthenticationStrategy myConcurrentSessionControlAuthenticationListener()
{
// we have to recreate the LogoutHandlers because we need to call them
// before invalidating the session
final LogoutHandler [] logoutHandlers = new LogoutHandler [] {
new CookieClearingLogoutHandler("JSESSIONID"),
new CsrfLogoutHandler(csrfTokenRepository())
//, new SecurityContextLogoutHandler() // seems to create problems with redirecting to the same page that caused the login request
};
SessionInvalidatingConcurrentSessionControlAuthenticationStrategy mine = new SessionInvalidatingConcurrentSessionControlAuthenticationStrategy(sessionRegistry(), logoutHandlers);
mine.setExceptionIfMaximumExceeded(false);
mine.setMaximumSessions(1);
return mine;
}
#Override
public void configure(WebSecurity web) throws Exception {
super.configure(web);
boolean devMode = this.env.acceptsProfiles("development");
final String [] ignoredPaths = devMode
? new String [] {"/webjars/**", "/static/**", "/bower_components/**" }
: new String [] {"/webjars/**", "/static/**" };
web
.ignoring()
.antMatchers(ignoredPaths)
.and()
.debug(false)
;
}
protected void configure(final HttpSecurity http) throws Exception {
super.configure(http);
http
.sessionManagement()
.maximumSessions(73467436) // this is just to trigger the ConcurrencyControlConfigurer
.sessionRegistry(sessionRegistry())
.and()
.withObjectPostProcessor(new ObjectPostProcessor<ConcurrentSessionControlAuthenticationStrategy>() {
#SuppressWarnings("unchecked")
#Override
public <O extends ConcurrentSessionControlAuthenticationStrategy> O postProcess(O concurrentSessionControlAS) {
// substitute the ConcurrentSessionControlAuthenticationStrategy created by
// ConcurrencyControlConfigurer with our own
return (O) myConcurrentSessionControlAuthenticationListener();
}
})
.and()
// we need to ignore the stomp endpoint to allow SockJS javascript client to issue POST requests
// to /push/../../.. when using trasports which are not WebSocket;
// at that time, protection is given by Stomp CSRF headers
.csrf()
.csrfTokenRepository(csrfTokenRepository())
.ignoringAntMatchers("/push/**")
.and()
// allow same origin to frame our site to support iframe SockJS
.headers()
.frameOptions().sameOrigin()
.and()
.authorizeRequests()
.antMatchers("/help/**").permitAll() // help redirects do not require authentication
.antMatchers("/push/info").permitAll() // do not require being authenticated for the /info request by SockJS
.anyRequest().authenticated()
.and()
// remove the session cookie when logging out
.logout()
.deleteCookies("JSESSIONID") // see: http://docs.spring.io/autorepo/docs/spring-security/current/reference/htmlsingle/#detecting-timeouts
.and()
;
}
}
AbstractCASWebSecurityConfigurerAdapter is an AbstractWebSecurityConfigurerAdapter that configures CAS.