I have a java that calls a Servlet:
public class UserServlet extends HttpServlet {
#Autowired
private UserService userService;
#Override
protected void service(final HttpServletRequest request, final HttpServletResponse response) throws ServletException, IOException {
userService.checkUser();
userService.doSomethingRestricted();
}
#Override
public void init(final ServletConfig config) throws ServletException {
SpringBeanAutowiringSupport.processInjectionBasedOnCurrentContext(this);
SpringBeanAutowiringSupport.processInjectionBasedOnServletContext(this, config.getServletContext());
super.init(config);
}
}
And my autowired service :
#Component(value = "userService")
public class UserService {
public boolean checkUser() {
if (SecurityContextHolder.getContext().getAuthentication() != null) {
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
if (auth != null && auth.getPrincipal() != null && auth.getPrincipal() instanceof User) {
User springUser = (User) auth.getPrincipal();
if (springUser != null) {
LOG.debug("USER CONNECTED :: {}", springUser.getUsername());
}
}
} else {
LOG.debug("NO CONNECTED USER, CREATING ONE");
Collection<GrantedAuthority> authorities = getGrantedAuthorities();
org.springframework.security.core.userdetails.User springUser = new org.springframework.security.core.userdetails.User("user","password", true, true, true, true, authorities);
Authentication auth = new UsernamePasswordAuthenticationToken(springUser, "", authorities);
SecurityContext sc = new SecurityContextImpl();
sc.setAuthentication(auth);
SecurityContextHolder.setContext(sc);
}
return true;
}
#Secured({ "CONNECTED" })
public void doSomethingRestricted() {
LOG.debug("SOMETHING RESTRICTED HAS BEEN DONE!!");
}
}
When I test my application the first time, the Java client sends a POST to the server, the server would check the user and would not find a context: a new context would be created.
When I run the java client the subsequent times, I find an existing Context (the one created in the first call).
Obviously there's something missing because If the first user logs in successfully it does not mean any user can connect.
What am I missing ? At first I thought about using sessions for each Java client's instance (I dont have web browser clients so I need to set the session ids manually), but when is Spring supposed to get or set the session id in the http request ?
TL;DR :
What does SecurityContextHolder.getContext().getAuthentication() do in my example ?
It gets you the authentication details of the current login user , have you added
<bean id="httpSessionFilter" class="org.springframework.security.web.context.SecurityContextPersistenceFilter"/>
to introduce login for web application , spring security is designed to work with POJO as well , you would need to add this filter in your mapping if you are doing it old way. If you are using http tags in applicationContext then it should work as it is.
</security:filter-chain-map>
Its been quite long since I have used spring security without the new http tags in applicatin Context . The spring security context comes with different filters , SecurityContextPersistenceFilter determines how the context is persisted.
"org.springframework.security.web.context.SecurityContextPersistenceFilter" is for persisting security context per session .
Spring security derived from its integration with acegi security which used to have "net.sf.acegisecurity.
ui.webapp.HttpSessionIntegrationFilter" filter for the same task
It is a filter , so spring can identify session based on sessionid.
Related
I have a question in relation with the combination of SwitchUser filter and authentication.
What I'm trying to achieve is to impersonate an existing user with the help of another user with elevated rights.
I was happy to find out that we can make use of the SwitchUserFilter provided by Spring Security, but when I tried to adapt it to my project and workflow it did not work as expected.
I have the following setup:
public class SecurityConfig extends WebSecurityConfigurerAdapter {
#Autowired
private CustomUserDetailsService customUserDetailsService;
#Bean
public TokenAuthenticationFilter tokenAuthenticationFilter() {
return new TokenAuthenticationFilter();
}
#Bean
public SwitchUserFilter switchUserFilter() {
var filter = new SwitchUserFilter();
filter.setUserDetailsService(customUserDetailsService);
filter.setSwitchUserUrl("/impersonate");
filter.setSwitchFailureUrl("/switchUser");
filter.setTargetUrl("/user"); // this is already implemented in my app (GET /user)
return filter;
}
#Override
protected void configure(HttpSecurity http) throws Exception {
// I've stripped this to the bare minimum which reproduces my flow
// usually I also configure authenticated access to endpoints & other security related configurations
http.csrf().disable();
http.addFilterBefore(tokenAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
http.addFilterAfter(switchUserFilter(), FilterSecurityInterceptor.class);
}
where
public class TokenAuthenticationFilter extends OncePerRequestFilter {
#Autowired
private TokenProvider tokenProvider;
#Autowired
private CustomUserDetailsService customUserDetailsService;
#Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
try {
String jwt = getJwtFromRequest(request);
if (StringUtils.hasText(jwt) && tokenProvider.validateToken(jwt)) {
Long userId = tokenProvider.getUserIdFromToken(jwt);
UserDetails userDetails = customUserDetailsService.loadUserById(userId);
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authentication);
}
} catch (Exception ex) {
log.error("Could not set user authentication in security context", ex);
}
filterChain.doFilter(request, response);
}
}
In the token I save the user id, and the user details service performs a findById in user's database.
The flow I perform is as follows:
login into application (call endpoint, use authentication manager to authenticate based on username/pass, generate token and return it)
call /impersonate?username=anotherUser with the Bearer token from previous step
the application reaches TokenAuthenticationFilter, decodes the token, finds the user in the database and updates the security context
after that it reaches the SwitchUserFilter where it performs the switch (finds user in database, creates user details and finally updates security context)
--- until this point everything works fine ---
the SwitchUser filter is configured to redirect to GET /user
in debug mode I see that at this point, we reach again step 3 (decode token, find user in db, update security context), but it does not perform the switch
application reaches GET /user with the updated user from previous step (original user, not the switched one)
My question is - how is this combo of authenticating users and switch user filter is supposed to work?
I feel that the flow is kinda natural and normal, the redirect and subsequent requests that will be made from outside need to be authenticated based on the bearer token.
How is this supposed to work? What am I missing/doing wrong?
Thanks!
Inside Spring boot security I'm trying to redirect server side the login page within Spring boot automatically to an overview page if I detect that the user is already logged in. The login screen should only show when the user is logged out.
#Configuration
public class MvcConfig implements WebMvcConfigurer
{
#Override
public void addInterceptors( InterceptorRegistry registry )
{
registry.addInterceptor( new LoginInterceptor() ).addPathPatterns( "/login" );
}
Inside LoginInterceptor I have:
#Override
public boolean preHandle( HttpServletRequest request, HttpServletResponse response, Object handler ) throws Exception
{
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
String url = request.getRequestURL().toString();
System.out.println(url);
if( auth.isAuthenticated() )
{
response.sendRedirect( "/my/overview" );
return false;
}
return true;
}
However, even if I am logged in, the debugger seems to think that I'm still on the /login route. Does spring boot route every request through /login for checking authentication? How do I achieve my aim of not resolving the login page unless the user is logged out?
Actually what was happening here was 'anonymousUser' was being returned for isAuthenticated, and thus the authenticated method wasn't working. The redirect for unauthorised accounts kicked in making me think that everything was going through login route when it was actually just the security doing its thing.
checking for principle null was the way to go instead.
if( auth.isAuthenticated() && principal != null )
{
response.sendRedirect( OVERVIEW_PAGE );
return false;
}
I am writing code of a game (web application) where I want to implement the following strategy. The user is allowed to login once. The second login results in warning message and a choice: to leave or to insist on logging in. If the user opts for logging in then all his previous sessions are to be expired. It works well thanks to Spring Security facilities:
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
...
.formLogin()
.failureHandler(new SecurityErrorHandler())
.and()
.sessionManagement()
.maximumSessions(1)
.maxSessionsPreventsLogin(true)
...
In case of the second login because of maxSessionsPreventsLogin(true) an exception is thrown. Then SecurityErrorHandler catches it and exercises redirection to the warning page:
public class SecurityErrorHandler extends SimpleUrlAuthenticationFailureHandler {
#Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
if (exception.getClass().isAssignableFrom(SessionAuthenticationException.class)) {
request.getRequestDispatcher("/double_login/"+request.getParameterValues("username")[0]).forward(request, response); //...the rest of the method
Until now everything is ok. If despite of warning the user choices to login the second time (he has probably closed browser without logging out with the first session going on unmanageably or so) he presses a button and then the controller calls the special service for making the previous sessions expired for this user (using his logged in username):
public void expireUserSessions(String username) {
for (Object principal : sessionRegistry.getAllPrincipals()) {
if (principal instanceof User) {
UserDetails userDetails = (UserDetails) principal;
if (userDetails.getUsername().equals(username)) {
for (SessionInformation information : sessionRegistry.getAllSessions(userDetails, true)) {
information.expireNow();
}
}
}
}
}
Moreover there is a SessionEventListener which (it doesn't matter because of logout, or natural expiring, or forced expiring by 'information.expireNow()') observes a session destroying event and implements a specific logic, such as saving user's persistent data, clean caches and so). This logic IS CRITICAL. The code which does it is the following:
public class SessionEventListener extends HttpSessionEventPublisher {
#Override
public void sessionCreated(HttpSessionEvent event) {
super.sessionCreated(event);
event.getSession().setMaxInactiveInterval(60*3);
}
#Override
public void sessionDestroyed(HttpSessionEvent event) {
String name=null;
SessionRegistry sessionRegistry = getSessionRegistry(event);
SessionInformation sessionInfo = (sessionRegistry != null ? sessionRegistry
.getSessionInformation(event.getSession().getId()) : null);
UserDetails ud = null;
if (sessionInfo != null) {
ud = (UserDetails) sessionInfo.getPrincipal();
}
if (ud != null) {
name=ud.getUsername();
//OUR IMPORTANT ACTIONS IN CASE OF SESSION CLOSING
getAllGames(event).removeByName(name);
}
super.sessionDestroyed(event);
}
public AllGames getAllGames(HttpSessionEvent event){
HttpSession session = event.getSession();
ApplicationContext ctx =
WebApplicationContextUtils.
getWebApplicationContext(session.getServletContext());
return (AllGames) ctx.getBean("allGames");
}
public SessionRegistry getSessionRegistry(HttpSessionEvent event){
HttpSession session = event.getSession();
ApplicationContext ctx =
WebApplicationContextUtils.
getWebApplicationContext(session.getServletContext());
return (SessionRegistry) ctx.getBean("sessionRegistry");
}
}
Then the hell happens. Despite my expectations the event for 'sessionDestroyed' method occurs not immediately after session expiring but ONLY AFTER THE USER LOGS IN FOR THE SECOND TIME (it is allowed as his previous session is expired by that moment, but to my surprise, this previous session isn't destroyed by Spring Security until now). So the logic implemented in the service 'getAllGames(event).removeByName(name)', which is called from 'sessionDestroyed', happens too late and, what is worse, after the user logs in for the second time. It breaks the logic.
I can implement different workarounds and so to say crutches. But please, if somebody know how to solve it directly, I would like you to advice me.
Remarks. I have called 'session.invalidate()' but it was also of no avail.
It is important for the already embodied logic that 'sessionDestroyed' in the SessionEventListener is triggered timely (immediately after session is expired). And frankly I don't know how to make it happens in the right and straightforward way.
I would appreciate your help and advice.
The only answer I'v found by myself was the following. An expired session is still not destroyed until the next HTTP request is sent within this session. Thus to kill an expired session you need to simulate such a request on behalf of the session. I have done it by creating and calling the following method:
void killExpiredSessionForSure(String sessionID) {
//sessionID - belongs to a session you want to kill
HttpHeaders requestHeaders = new HttpHeaders();
requestHeaders.add("Cookie", "JSESSIONID=" + sessionID);
HttpEntity requestEntity = new HttpEntity(null, requestHeaders);
RestTemplate rt = new RestTemplate();
rt.exchange("http://localhost:8080", HttpMethod.GET, requestEntity, String.class);
}
After this method is called the event 'sessionDestroyed' is properly issued and handled, and an expired session no longer exists.
I am using Spring 4 to create an API and I need to have authentication for the API requests.
Currently, I have created a HandlerInterceptorAdapter to pick out authentication related headers and perform some validation on those values.
If everything is OK, I set the SecurityContext to a custom implementation of Authentication then in the postHandle I set the authentication to null.
Everything works great, except I keep getting warnings in Tomcat7 about ThreadLocal variables not being removed when the application shuts down.
SEVERE: The web application [] created a ThreadLocal with key of type [java.lang.ThreadLocal] (value [java.lang.ThreadLocal#6c3e4fdb]) and a value of type [org.springframework.security.core.context.SecurityContextImpl] (value [org.springframework.security.core.context.SecurityContextImpl#ffffffff: Null authentication]) but failed to remove it when the web application was stopped. Threads are going to be renewed over time to try and avoid a probable memory leak.
I get that I may be doing this totally wrong, if so I would love some direction. :D
Here is my interceptor:
/**
* Intercepts Requests to set the Authentication in the SecurityContext.
* Sets the response to 401 - Unauthorized, if the header is missing
*/
#Component
public class AuthenticationHandlerInterceptor extends HandlerInterceptorAdapter {
private HandlerMediator mediator;
Logger log = LoggerFactory.getLogger(AuthenticationHandlerInterceptor.class);
#Autowired
public AuthenticationHandlerInterceptor(HandlerMediator mediator) {
this.mediator = mediator;
}
#Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String username = request.getHeader("authentication-username");
String token = request.getHeader("authentication-token");
// if the remote host is local, then override the authentication
if (request.getRemoteHost().equals("0:0:0:0:0:0:0:1") || request.getRemoteHost().equals("127.0.0.1")) {
log.info("On localhost, overriding authentication with localhost");
username = "TEST";
token = "localhost:localhost";
}
if (username == null || username.trim().length() == 0) {
failAuthentication(response, "Missing Authentication Username Header");
return false;
}
if (token == null || token.trim().length() == 0 || !token.contains(":")) {
failAuthentication(response, "Missing Authentication Token Header");
return false;
}
String[] keys = token.split(":");
String appName = keys[0];
String apikey = keys[1];
if (!appName.equals("localhost")) {
// we are not under localhost so we have to authenticate the application calling us
if (mediator.executeCommand(new AuthenticateApplicationCommand(appName, apikey)) == false) {
failAuthentication(response, "Application Token failed authentication");
return false;
}
}
SecurityContextHolder.getContext().setAuthentication(new ApiAuthentication(username, appName));
return true;
}
#Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
SecurityContextHolder.getContext().setAuthentication(null);
}
private void failAuthentication(HttpServletResponse response, String message) throws Exception {
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.setContentType("text/html;charset=UTF-8");
ServletOutputStream out = response.getOutputStream();
out.println(message);
out.close();
}
}
How do I get rid of these warnings?
Thanks,
Joe
To do authentication based on the content of HTTP headers, the framework foresees a defining custom authentication filter, plugged in the spring security chain via configuration similar to this (see also this answer):
<security:http>
...
<security:custom-filter ref="customAuthenticationFilter" after="SECURITY_CONTEXT_FILTER" />
</security:http>
There in this filter it's possible to add code similar to this (see also BasicAuthenticationFilter):
try {
....
SecurityContextHolder.getContext().setAuthentication(authResult);
} catch (AuthenticationException failed) {
SecurityContextHolder.clearContext();
}
Have a look at class ThreadLocalSecurityContextHolderStrategy,it's there that the SecurityContextImpl instance is getting stored in a ThreadLocal and cleared.
You could try to call clearContext() in AuthenticationHandlerInterceptor, but it's probably better to use the hook the framework foresees as that should lead to less surprises similar to the one you reported.
Our project is Spring + Spring Security + Maven + Hibernate + JSF + Icefaces
We need to define values for currentUser, but it cannot be defined until after the user has logged in.
public static UserDetails currentUserDetails(){
SecurityContext securityContext = SecurityContextHolder.getContext();
Authentication authentication = securityContext.getAuthentication();
if (authentication != null) {
Object principal = authentication.getPrincipal();
return (UserDetails) (principal instanceof UserDetails ? principal : null);
}
return null;
}
If we try to include definition in server startup, a nullpointerException is thrown(as expected).
Our current method is to lazily define it in the getter method. So whenever the JSF calls it, an HQL is ran but this is very expensive.
Could someone give advice on how to run currentUserDetails method only once AFTER the user has already logged in?
If you use a <form-login> tag in your security.xml file - you can define success login callback, like
<form-login
authentication-success-handler-ref="authenticationSuccessHandler"/>
<bean id="authenticationSuccessHandler" class="com.test.MyAuthenticationSuccessHandler"/>
and bean impl
public class MyAuthenticationSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler {
#Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) {
// use authentication object to fetch the user object and update its timestamp
}
}
I hope it will be helpful. Good luck.