I have a class :
#SessionScoped
public class LoggedUser {
private User user;
...
}
that I use to keep track if a user is logged in my application.
In my Struts2 application I have a Interceptor to check if the user is logged, if not he's forwarded to the login page.
public class LoggedUserInterceptor extends AbstractInterceptor {
private static final long serialVersionUID = 2822434409188961460L;
#Inject
private LoggedUser loggedUser;
#Override
public String intercept(ActionInvocation invocation) throws Exception {
if(loggedUser==null || !loggedUser.isLogged()){
return "login";
}
return invocation.invoke();
}
}
The problem occur when the session timeout. The object LoggdeUser is never null or deleted. I have always the last instance.
I added A session listener.
public class SessionListener implements HttpSessionListener {
private static Logger logger = Logger.getLogger(SessionListener.class.getName());
#Override
public void sessionCreated(HttpSessionEvent event) {
logger.info("sessionCreated = " + event.getSession().getId());
}
#Override
public void sessionDestroyed(HttpSessionEvent event) {
logger.info("sessionDestroyed = " + event.getSession().getId());
}
}
I see that sessionDestroyed is called, but when I enter again in my Interceptor.. LoggedUser is never recreated for a new session.
why ?
my Struts2 Action for the login is this
public class LoginUserAction extends ActionSupport implements ModelDriven<LoggedUser>, ServletRequestAware {
...
#Inject
private LoggedUser loggedUser;
public String execute() throws Exception {
...
loggerUser.setLoggedTime(now);
...
return SUCCESS;
}
I add that too in web.xml
session-config
session-timeout 2 /session-timeout
/session-config
I don't know anything about Struts2, but my guess is that the interceptor's scope is wider than session scope. In other words, the interceptor instance is kept around longer than the session. Guice can't and won't set an injected field to null when the session ends, nor will it ever re-inject an object automatically.
What you need to do if you use an object with a shorter lifecycle inside an object with a longer lifecycle (such as a RequestScoped object inside a SessionScoped object or a SessionScoped object inside a singleton) is inject a Provider for the shorter lived object.
In your case, I think this is probably what you need:
public class LoggedUserInterceptor extends AbstractInterceptor {
private static final long serialVersionUID = 2822434409188961460L;
#Inject
private Provider<LoggedUser> loggedUserProvider;
#Override
public String intercept(ActionInvocation invocation) throws Exception {
LoggedUser loggedUser = loggedUserProvider.get();
if(loggedUser==null || !loggedUser.isLogged()){
return "login";
}
return invocation.invoke();
}
}
I don't know guice but the session is available to the interceptor and can be made readily available to the action via SessionAware :
ActionInvocation provides access to the session. The following is part of a "Authentication" interceptor. If there is not a "User" object then Action.LOGIN is returned.
public String intercept(ActionInvocation invocation) throws Exception {
Map session = invocation.getInvocationContext().getSession();
appLayer.User user = (appLayer.User) session.get("User");
if (user == null){
return Action.LOGIN;
}
return invocation.invoke();
}
I only place the user object on the session when the user logs in.
In the login action I place the user object on the session:
public class Login extends ActionSupport implements SessionAware {
private Map<String, Object> session;
private String userName;
private String password;
public String execute() {
//use DB authentication once this works
//if ("ken".equalsIgnoreCase(userName) && "ken".equalsIgnoreCase(password)){
try {
User user = new User(userName, password);
session.put("User", user);
return ActionSupport.SUCCESS;
} catch (Exception e) {
System.out.println("There was an exception" + e.getMessage());
return ActionSupport.LOGIN;
}
}
public void validate() {
String user = this.getUserName();
String pass = this.getPassword();
//This block is a bit redundant but I couldn't figure out how
//to get just the hibernate part to report an exception on Username/pass
if (!StringUtils.isBlank(this.getUserName()) && !StringUtils.isBlank(this.getPassword())) {
try {
Class.forName("com.ibm.as400.access.AS400JDBCDriver").newInstance();
String url = "jdbc:as400://192.168.1.14";
DriverManager.getConnection(url, user, pass).close();
} catch (Exception e) {
addFieldError("login.error.authenticate", "Bad Username / Password");
}
}
if (StringUtils.isBlank(getUserName())) {
addFieldError("login.error.name", "Missing User Name");
}
if (StringUtils.isBlank(getPassword())) {
addFieldError("login.error.password", "Missing Password");
}
//else both are blank don't report an error at this time
}
... getters/setters....
}//end class
If I remember correctly this comes from at least in part from "Struts2 in Action". Anyways I'm a big believer in DI but since the Session is pretty accessible from the interceptors and the action I don't bother.
the easiest way to do that is finally to put the token in the session at the login and check it with an Interceptor
#Override
public String intercept(ActionInvocation invocation) throws Exception {
loggedUser = (LoggedUser) invocation.getInvocationContext().getSession().get(LoggedUser.SESSIONTOKEN);
if(loggedUser==null || !loggedUser.isLogged()){
logger.info("loggedUser not present in session");
return "login";
}
return invocation.invoke();
}
in the Action
request.getSession().setAttribute(LoggedUser.SESSIONTOKEN, loggedUser);
Related
I use keycloak as a Central Authentication Service for (single sign on/out) feature.
I have app1, app2, app3. app1 and app2 is monothetic application. app3 use spring session (use redis as session store),
All feature work fine. But I use the back channel to logout for SSO(single sign out) feature, that's works for app1 and app2. But it not work for this app3.
I wonder how to back channel logout application that use spring session
The keycloak admin url invoke when client user send a logout request to it.I find that KeycloakAutoConfiguration#getKeycloakContainerCustomizer() inject WebServerFactoryCustomizer for add KeycloakAuthenticatorValve, and that Valve
use CatalinaUserSessionManagement, but it have not any info about redis as its session store. So I add a customizer for enhence the Valve.
first i set the order of the autoconfig, because extra customizer must be callback after it.
#Slf4j
#Component
public class BeanFactoryOrderWrapper implements DestructionAwareBeanPostProcessor {
#Override
public void postProcessBeforeDestruction(Object bean, String beanName) throws BeansException {
}
#Override
public boolean requiresDestruction(Object bean) {
return true;
}
#Override
public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
if (beanName.equals("getKeycloakContainerCustomizer")) {
Object wrapRes = this.wrapOrder(bean);
return wrapRes;
}
return bean;
}
#Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
return bean;
}
private Object wrapOrder(Object bean) {
log.info("rewrite keycloak auto config customizer Order for next custom");
final WebServerFactoryCustomizer origin = (WebServerFactoryCustomizer) bean;
return new KeycloakContainerCustomizerWithOrder(origin);
}
}
class KeycloakContainerCustomizerWithOrder implements WebServerFactoryCustomizer<ConfigurableServletWebServerFactory>, Ordered {
private final WebServerFactoryCustomizer origin;
public KeycloakContainerCustomizerWithOrder(WebServerFactoryCustomizer origin) {
this.origin = origin;
}
#Override
public void customize(ConfigurableServletWebServerFactory factory) {
origin.customize(factory);
}
#Override
public int getOrder() {
return Ordered.LOWEST_PRECEDENCE - 1;
}
}
I extra RedisIndexedSessionRepository, and set it to proxy object
#Slf4j
#Configuration
#RequiredArgsConstructor
class ContainerConfig {
private final RedisIndexedSessionRepository sessionRepository;
#Bean
public WebServerFactoryCustomizer<ConfigurableServletWebServerFactory> getKeycloakContainerCustomizerGai() {
return configurableServletWebServerFactory -> {
if (configurableServletWebServerFactory instanceof TomcatServletWebServerFactory) {
TomcatServletWebServerFactory container = (TomcatServletWebServerFactory) configurableServletWebServerFactory;
container.getContextValves().stream().filter(ele -> ele.getClass() == KeycloakAuthenticatorValve.class).findFirst().map(ele -> (AbstractKeycloakAuthenticatorValve) ele).ifPresent(valve -> {
try {
final Field field = AbstractKeycloakAuthenticatorValve.class.getDeclaredField("userSessionManagement");
field.setAccessible(true);
final CatalinaUserSessionManagement origin = (CatalinaUserSessionManagement) field.get(valve);
field.set(valve, new CatalinaUserSessionManagementGai(origin, sessionRepository));
} catch (Exception e) {
log.error("enhence valve fail");
}
});
}
};
}
}
#Slf4j
class CatalinaUserSessionManagementGai extends CatalinaUserSessionManagement {
private final CatalinaUserSessionManagement origin;
private final RedisIndexedSessionRepository sessionRepository;
public CatalinaUserSessionManagementGai(CatalinaUserSessionManagement origin, RedisIndexedSessionRepository sessionRepository) {
this.origin = origin;
this.sessionRepository = sessionRepository;
}
public void login(Session session) {
origin.login(session);
}
public void logoutAll(Manager sessionManager) {
origin.logoutAll(sessionManager);
}
public void logoutHttpSessions(Manager sessionManager, List<String> sessionIds) {
for (String sessionId : sessionIds) {
logoutSession(sessionManager, sessionId);
}
}
protected void logoutSession(Manager manager, String httpSessionId) {
try {
final Method method = CatalinaUserSessionManagement.class.getDeclaredMethod("logoutSession", Manager.class, String.class);
method.setAccessible(true);
method.invoke(origin,manager,httpSessionId);
} catch (Exception e) {
log.error("session manager proxy invoke error");
}
// enhence part
sessionRepository.deleteById(httpSessionId);
}
protected void logoutSession(Session session) {
try {
final Method method = CatalinaUserSessionManagement.class.getDeclaredMethod("logoutSession", Session.class);
method.setAccessible(true);
method.invoke(origin,session);
} catch (Exception e) {
log.error("session manager proxy invoke error");
}
}
public void sessionEvent(SessionEvent event) {
origin.sessionEvent(event);
}
}
that work for me
I understand that in order to keep the context, the reactive chain of methods must not be broken. However, I need to get access to the context from the ExceptionResolver (after an exception has been thrown.)
My exception resolver is extending AbstractErrorWebExceptionHandler and when I try to get the context via ReactiveSecurityContextHolder.getContext() it returns empty. Obviously because the reactive chain has been broken.
How can I get access to the authentication object?
You can get access to the authentication object by overriding the handle method:
public class TestHandler extends AbstractErrorWebExceptionHandler {
public TestHandler(ErrorAttributes errorAttributes, ResourceProperties resourceProperties,
ApplicationContext applicationContext) {
super(errorAttributes, resourceProperties, applicationContext);
}
#Override
protected RouterFunction<ServerResponse> getRoutingFunction(ErrorAttributes errorAttributes) {
return null;
}
#Override
public Mono<Void> handle(ServerWebExchange exchange, Throwable throwable) {
Mono<Principal> authObject = exchange.getPrincipal();
//Handle exception here
return exchange.getResponse().setComplete();
}
}
Another approach is to implement the ErrorWebExceptionHandler directly:
public class TestHandler implements ErrorWebExceptionHandler {
#Override
public Mono<Void> handle(ServerWebExchange serverWebExchange, Throwable throwable) {
Mono<Principal> authObject = serverWebExchange.getPrincipal();
//Handle exception here
return serverWebExchange.getResponse().setComplete();
}
}
I have been able to get a Spring Security based application up and running, and it has been satisfying all my requirements until now.
I do have 1 doubt regarding how UserDetailsService is used in Spring Security. I have a custom 'UserDetailsService' implementation, which goes like this -
public class CustomUserDetailsService implements UserDetailsService {
#Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
Optional<UserDetailsDto> userDetailsByEmail = // Load userDetailsDto from database
if (!userDetailsByEmail.isPresent()) {
throw new UsernameNotFoundException("Username does not exists");
}
UserDetailsDto userDetailsDto = userDetailsByEmail.get();
List<Role> roles = roleService.listByEmail(username);
List<ModulePermission> modulePermissions = modulePermissionService.listByUserId(userDetailsDto.getId());
UserType userType = userTypeService.getByUserId(userDetailsDto.getId());
return new LoggedInUser(userDetailsDto, roles, modulePermissions, userType);
}
}
The class LoggedInUser is an extension of Spring Security's org.springframework.security.core.userdetails.User class, which goes like this -
public class LoggedInUser extends User {
private static final long serialVersionUID = -1L;
private Long userId;
private boolean firstLogin;
private UserType userType;
private List<ModulePermission> modulePermissions;
private String firstName;
private String lastName;
private String contactNo;
public LoggedInUser(UserDetailsDto userDetailsDto, List<Role> roles, List<ModulePermission> modulePermissions,
UserType userType) {
super(userDetailsDto.getEmail(), userDetailsDto.getPassword(), userDetailsDto.getEnabledStatus().getValue(),
userDetailsDto.getAccountNonExpiredStatus().getValue(), true,
userDetailsDto.getAccountNonLockedStatus().getValue(),
roles.stream().map(role -> new SimpleGrantedAuthority(role.getId())).collect(Collectors.toList()));
this.modulePermissions = modulePermissions;
this.userType = userType;
this.userId = userDetailsDto.getId();
this.firstLogin = userDetailsDto.getIsFirstLoginStatus().getValue();
this.firstName = userDetailsDto.getFirstName();
this.lastName = userDetailsDto.getLastName();
this.contactNo = userDetailsDto.getContactNo();
}
public List<ModulePermission> getModulePermissions() {
return Collections.unmodifiableList(modulePermissions);
}
public UserType getUserType() {
return userType;
}
public Long getUserId() {
return userId;
}
public boolean isFirstLogin() {
return firstLogin;
}
public String getFirstName() {
return firstName;
}
public String getLastName() {
return lastName;
}
public String getContactNo() {
return contactNo;
}
public void setFirstLogin(boolean firstLogin) {
this.firstLogin = firstLogin;
}
}
Now, to configure Spring Security to use my CustomUserDetailsService, I do the following in security configuration -
#Bean
public UserDetailsService customUserDetailsService() {
return new CustomUserDetailsService();
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http.antMatcher(SuperAdminConstant.UrlConstant.ANT_MATCHER_PATH)
.userDetailsService(customUserDetailsService())
.formLogin(// further configuration)
}
And this works without any problems.
But notice that in CustomUserDetailsService, several database queries are executed even before the user has been authenticated successfully (This is because Spring Security has created a DaoAuthenticationProvider, which loads a UserDetails implementation (in my case, LoggedInUser), and perform various checks on that object AFTER it has been retrieved from a UserDetailsService (in my case , CustomUserDetailsService)).
Consider that a user has entered the correct username, but a wrong password. In that case, the high-level authentication flow would be -
CustomUserDetailsService would be called
First query is executed to verify username and load user details (UsernameNotFoundException is not thrown as username is correct)
Second query is executed to retrieve the roles
Third query is executed to retrieve module permissions
Fourth query is executed to retrieve user types
DaoAuthenticationProvider checks the password, finds it to be incorrect, and throws a BadCredentialsException.
So as can be seen, total 4 queries are executed EVEN BEFORE authentication process has completed, out of which only 1 is essential at this stage (the first query to verify username).
One solution to this problem can be to eliminate the use of UserDetailsService altogeather, and use a custom AuthenticationProvider instead.
public class CustomAuthenticationProvider implements AuthenticationProvider {
#Override
Authentication authenticate(Authentication authentication)
throws AuthenticationException {
// Customize the authentication logic here, and retrieve
// user information only if everything is correct.
}
}
But going by this approach also means that I have to duplicate code and functionality provided by DaoAuthenticationProvider and AbstractUserDetailsAuthenticationProvider, which includes checking user account status flags manually (accountNonExpired, accountNonLocked etc.), and throwing exceptions.
So I was wondering weather it is possible to perform authentication logic in such a way that user information is retrieved only AFTER authentication succeeds, and most of the authentication logic provided by Spring Security can be resused.
Any ideas will be deeply appreciated.
You can write implementation of AuthenticationSuccessHandler:
#Component
public class AuthenticationSuccessHandlerImpl implements AuthenticationSuccessHandler {
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
LoggedInUser loggedInUser = (LoggedInUser)authentication.getPrincipal();
List<Role> roles = roleService.listByEmail(username);
List<ModulePermission> modulePermissions = modulePermissionService.listByUserId(userDetailsDto.getId());
UserType userType = userTypeService.getByUserId(userDetailsDto.getId());
// Set roles after authentication succeeds
loggedInUser.setRoles(roles);
loggedInUser.setModulePermissions(modulePermissions);
loggedInUser.setUserType(userType);
}
}
After the authentication succeeds, you can obtain logged in user from security context and set additional properties.
I'm writing Grails application with Spring Security plugin.
I have enabled queries generated by GORM into console and I have noticed that every request Security query the database about users, selecting them by it's username.
My intention is load user's by it's ID's, not usernames for increase performance.
I am aware that there is a possibility to overwrite UserDetailsService method loadUserByUsername(String username), but this method is used both for refreshing user's credential during the session and in login form, where in fact I want to authenticate user by it's username.
I have three questions:
How to load user by id? Should I inject user ID instead of username in GrailsUser (implementation of UserDetails) instead of regular username and use long selectById = Long.valueOf(String username)?
How to create different user provider for refreshing session to grab user by ID and different for user login (when I want to grab user by it's username/email)?
There is possibility to fetch user credentials not every request, but every X seconds?
Finally i managed to solve this problem. The queries are generated by:
springSecurityService.getCurrentUser()
Unfortunatelly, this method fetches User model class by username (from Principal object) and maps it into database field, at most configured by:
grails.plugin.springsecurity.userLookup.usernamePropertyName
as mentioned in documentation.
I have tried
grails.plugin.springsecurity.userLookup.usernamePropertyName = 'id'
but i received class-cast exception from String to Long.
Workaround is simple - create own Principle with username field typed as Long.
See PrincipalProxy in my solution:
package com.selly.util.security
import java.security.Principal;
import grails.plugin.springsecurity.userdetails.GrailsUser
import org.springframework.security.core.Authentication
import org.springframework.security.core.GrantedAuthority
class AppMetadataAuthenticationToken implements Authentication, Principal {
private boolean authenticated
private GrailsUser userDetails
private Principal principal
public AppMetadataAuthenticationToken(GrailsUser userDetails) {
this.userDetails = userDetails
this.principal = new PrincipalProxy(userDetails)
}
public GrailsUser getUser() {
return userDetails
}
public String getUsername() {
return userDetails.getUsername()
}
#Override
public String getName() {
return userDetails.getUsername()
}
#Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return userDetails.getAuthorities()
}
#Override
public Object getCredentials() {
return userDetails.password
}
#Override
public Object getDetails() {
return getUser()
}
#Override
public Object getPrincipal() {
return principal
}
#Override
public boolean isAuthenticated() {
return authenticated
}
#Override
public void setAuthenticated(boolean authenticated) throws IllegalArgumentException {
this.authenticated = authenticated
}
static class PrincipalProxy implements Principal {
GrailsUser grailsUser
Long username
public PrincipalProxy(GrailsUser grailsUser) {
this.grailsUser = grailsUser
this.username = grailsUser.id
}
#Override
public String getName() {
return grailsUser.id
}
}
}
To return this Token, just register your own AuthenticationProvider:
package com.selly.util.security;
import grails.plugin.springsecurity.SpringSecurityService
import org.springframework.security.authentication.AuthenticationProvider
import org.springframework.security.authentication.BadCredentialsException
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken
import org.springframework.security.authentication.dao.DaoAuthenticationProvider
import org.springframework.security.core.Authentication
import org.springframework.security.core.AuthenticationException
import org.springframework.security.core.userdetails.UserDetailsService
import org.springframework.security.core.userdetails.UsernameNotFoundException
public class AppUsernamePasswordAuthenticationProvider extends DaoAuthenticationProvider implements AuthenticationProvider {
SpringSecurityService springSecurityService
#Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
def token = (UsernamePasswordAuthenticationToken) authentication
def user = userDetailsService.loadUserByUsername(authentication.principal)
if(!user)
throw new UsernameNotFoundException("Cannot find user", authentication.principal)
if(!passwordEncoder.isPasswordValid(user.password, authentication.credentials, null))
throw new BadCredentialsException("Invalid password")
return new AppMetadataAuthenticationToken(user)
}
#Override
public boolean supports(Class<?> authentication) {
return UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication);
}
}
package com.selly.util.security;
import org.springframework.security.authentication.AuthenticationProvider
import org.springframework.security.core.Authentication
import org.springframework.security.core.AuthenticationException
public class AppMetadataAuthenticationProvider implements AuthenticationProvider {
#Override
public Authentication authenticate(Authentication authentication)
throws AuthenticationException {
// TODO Auto-generated method stub
return authentication;
}
#Override
public boolean supports(Class<?> authentication) {
// TODO Auto-generated method stub
return AppMetadataAuthenticationToken.class.isAssignableFrom(authentication);
}
}
Register it in resources.groovy
appUsernamePasswordAuthenticationProvider(AppUsernamePasswordAuthenticationProvider) {
userDetailsService = ref('userDetailsService')
passwordEncoder = ref('passwordEncoder')
userCache = ref('userCache')
saltSource = ref('saltSource')
preAuthenticationChecks = ref('preAuthenticationChecks')
postAuthenticationChecks = ref('postAuthenticationChecks')
springSecurityService = ref('springSecurityService')
}
And in Config.groovy:
grails.plugin.springsecurity.providerNames = [
'appMetadataAuthenticationProvider',
'appUsernamePasswordAuthenticationProvider',
// 'daoAuthenticationProvider',
// 'anonymousAuthenticationProvider',
// 'rememberMeAuthenticationProvider'
]
Now all works perfectly:
Hibernate: select this_.id as id13_0_, this_.account_expired as account2_13_0_, this_.account_locked as account3_13_0_, this_.enabled as enabled13_0_, this_."password" as password5_13_0_, this_.password_expired as password6_13_0_, this_.username as username13_0_, this_.workspace as workspace13_0_ from users this_ where (**this_.id=?**) limit ?
Instead of using getCurrentUser() you can also getPrincipal() and cast to your previously populated object with more data than Principal interface offers.
How to generate "intercept-url" dynamically. My user name and roles are stored in database,
I want to map all these users in to spring security.Is there any way to do this?
You'll have to provide your own implementation of com.icod.solapCore.spring.security.FilterInvocationSecurityMetadataSource.
This could look like this :
public class MyFilterInvocationSecurityMetadataSource implements FilterInvocationSecurityMetadataSource {
#Override
public Collection<ConfigAttribute> getAttributes(Object object) throws IllegalArgumentException {
FilterInvocation filterInvocation = (FilterInvocation) object;
HttpServletRequest request = filterInvocation.getHttpRequest();
Collection<ConfigAttribute> result = new ArrayList<ConfigAttribute>();
// Find roles in database that secures the specified request
// ...
// For any role found, create a SecurityConfig object prefixed with "ROLE_" ex :
// for(String role : roles) {
// ConfigAttribute attribute = new SecurityConfig("ROLE_"+roleFound);
// result.add(attribute);
// }
return result;
}
#Override
public Collection<ConfigAttribute> getAllConfigAttributes() {
return null;
}
#Override
public boolean supports(Class<?> clazz) {
return FilterInvocation.class.isAssignableFrom(clazz);
}
}
And then you'll have to replace the default FilterInvocationSecurityMetadataSource with your own. I do it with a BeanPostProcessor, called after spring read the configuration file but before it makes the configuration official. Looks like this :
public class MyFilterInvocationSecurityMetadataSourceBeanPostProcessor implements BeanPostProcessor {
private FilterInvocationSecurityMetadataSource metadataSource = new MyFilterInvocationSecurityMetadataSource();
#Override
public Object postProcessBeforeInitialization(Object bean, String name) throws BeansException {
if (bean instanceof FilterInvocationSecurityMetadataSource) {
return metadataSource;
}
return bean;
}
#Override
public Object postProcessAfterInitialization(Object bean, String name) throws BeansException {
return bean;
}
}
Then you just have to configure the bean post processor :
<bean id="solapcoreFilterInvocationSecurityMetadataSourceBeanPostProcessor" class="foo.bar.MyFilterInvocationSecurityMetadataSourceBeanPostProcessor"/>
Hope this help.
Give all your users same role and operate with this role in config.
You can read abour roles here