Start a session from a given id in spring - spring

How can I create a session in a spring mvc application from a given ID instead of a generated one?
I want to fixate the session.
The fixation will be started by a trusted ui service. This trusted service forwards all requests. Thus the fixation can't begin in browser. It is not intended to do it without this ui service.
Providing a HttpSessionIdResolver bean does not work, since it only changes the location in HTTP response. Eg Session ID in HTTP header after authorization.
Are there any solutions without creating a shadow session management?
Session is required for keycloak integration. Guess it's not possible to use keycloak in stateless mode.
Thanks in advance

It is possible to fixate a session with spring-session.
#Configuration
#EnableSpringHttpSession
public class SessionConfig {
#Bean
public HttpSessionIdResolver httpSessionIdResolver() {
return new HttpSessionIdResolver() {
public List<String> resolveSessionIds(HttpServletRequest request) {
final var sessionId = request.getHeader("X-SessionId");
request.setAttribute(SessionConfig.class.getName() + "SessionIdAttr", sessionId);
return List.of(sessionId);
}
public void setSessionId(HttpServletRequest request, HttpServletResponse response, String sessionId) {}
public void expireSession(HttpServletRequest request, HttpServletResponse response) {}
};
}
#Bean
public SessionRepository<MapSession> sessionRepository() {
return new MapSessionRepository(new HashMap<>()) {
#Override
public MapSession createSession() {
var sessionId =
(String)RequestContextHolder
.currentRequestAttributes()
.getAttribute(SessionConfig.class.getName()+"SessionIdAttr", 0);
final var session = super.createSession();
if (sessionId != null) {
session.setId(sessionId);
}
return session;
}
};
}
In #resolveSessionIds() you can read the ID and store for later use.
createSession() is called when no session has been found and a new one is required. Here you can create the session with previously remembered ID in MapSession#setId(String).
Even if is possible, IMO it is not a good idea. There might be other architectural solutions/problems.

Related

How to implement multi-tenancy in new Spring Authorization server

Link for Authorization server: https://github.com/spring-projects/spring-authorization-server
This project pretty much has everything in terms of OAuth and Identity provider.
My question is, How to achieve multi-tenancy at the Identity provider level.
I know there are multiple ways to achieve multi-tenancy in general.
The scenario I am interested in is this:
An organization provides services to multiple tenants.
Each tenant is associated with a separate database (Data isolation including user data)
When a user visits dedicated Front-end app(per tenant) and negotiate access tokens from Identity provider
Identity provider then identifies tenant (Based on header/ Domain name) and generates access token with tenant_id
This access token then is passed on to down-stream services, which intern can extract tenant_id and decide the data source
I have a general idea about all the above steps, but I am not sure about point 4.
I am not sure How to configure different data sources for different tenants on the Identity Provider? How to add tenant_id in Token?
Link to the issue: https://github.com/spring-projects/spring-authorization-server/issues/663#issue-1182431313
This is not related to Spring auth Server, but related to approaches that we can think for point # 4
I remember the last time we implemented a similar approach, where we had below options
To have unique email addresses for the users thereby using the global database to authenticate the users and post authentication, set up the tenant context.
In case of users operating in more than 1 tenant, post authentication, we can show the list of tenant's that the user has access to, which enables setting the tenant context and then proceeding with the application usage.
More details can be read from here
This is really a good question and I really want to know how to do it in new Authorization Server in a proper way. In Spring Resource Server there is a section about Multitenancy. I did it successfully.
As far as new Spring Authorization Server multitenancy concerns. I have also done it for the password and the Client Credentials grant type.
But please note that although it is working but how perfect is this. I don't know because I just did it for learning purpose. It's just a sample. I will also post it on my github when I would do it for the authorization code grant type.
I am assuming that the master and tenant database configuration has been done. I can not provide the whole code here because it's lot of code. I will just provide the relevant snippets. But here is just the sample
#Configuration
#Import({MasterDatabaseConfiguration.class, TenantDatabaseConfiguration.class})
public class DatabaseConfiguration {
}
I used the separate database. What I did I used something like the following in the AuthorizationServerConfiguration.
#Import({OAuth2RegisteredClientConfiguration.class})
public class AuthorizationServerConfiguration {
#Bean
#Order(Ordered.HIGHEST_PRECEDENCE)
public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception {
OAuth2AuthorizationServerConfigurer<HttpSecurity> authorizationServerConfigurer = new OAuth2AuthorizationServerConfigurer<>();
....
http.addFilterBefore(new TenantFilter(), OAuth2AuthorizationRequestRedirectFilter.class);
SecurityFilterChain securityFilterChain = http.formLogin(Customizer.withDefaults()).build();
addCustomOAuth2ResourceOwnerPasswordAuthenticationProvider(http);
return securityFilterChain;
}
}
Here is my TenantFilter code
public class TenantFilter extends OncePerRequestFilter {
private static final Logger LOGGER = LogManager.getLogger();
#Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
String requestUrl = request.getRequestURL().toString();
if (!requestUrl.endsWith("/oauth2/jwks")) {
String tenantDatabaseName = request.getParameter("tenantDatabaseName");
if(StringUtils.hasText(tenantDatabaseName)) {
LOGGER.info("tenantDatabaseName request parameter is found");
TenantDBContextHolder.setCurrentDb(tenantDatabaseName);
} else {
LOGGER.info("No tenantDatabaseName request parameter is found");
response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
response.getWriter().write("{'error': 'No tenant request parameter supplied'}");
response.getWriter().flush();
return;
}
}
filterChain.doFilter(request, response);
}
public static String getFullURL(HttpServletRequest request) {
StringBuilder requestURL = new StringBuilder(request.getRequestURL().toString());
String queryString = request.getQueryString();
if (queryString == null) {
return requestURL.toString();
} else {
return requestURL.append('?').append(queryString).toString();
}
}
}
Here is the TenantDBContextHolder class
public class TenantDBContextHolder {
private static final ThreadLocal<String> TENANT_DB_CONTEXT_HOLDER = new ThreadLocal<>();
public static void setCurrentDb(String dbType) {
TENANT_DB_CONTEXT_HOLDER.set(dbType);
}
public static String getCurrentDb() {
return TENANT_DB_CONTEXT_HOLDER.get();
}
public static void clear() {
TENANT_DB_CONTEXT_HOLDER.remove();
}
}
Now as there is already configuration for master and tenant database. In these configurations we also check for the TenantDBContextHolder
class that it contains the value or not. Because when request comes for token then we check the request and set it in TenantDBContextHolder. So base on this thread local variable right database is connected and the token issue to the right database. Then in the token customizer. You can use something like the following
public class UsernamePasswordAuthenticationTokenJwtCustomizerHandler extends AbstractJwtCustomizerHandler {
....
#Override
protected void customizeJwt(JwtEncodingContext jwtEncodingContext) {
....
String tenantDatabaseName = TenantDBContextHolder.getCurrentDb();
if (StringUtils.hasText(tenantDatabaseName)) {
URL issuerURL = jwtClaimSetBuilder.build().getIssuer();
String issuer = issuerURL + "/" + tenantDatabaseName;
jwtClaimSetBuilder.claim(JwtClaimNames.ISS, issuer);
}
jwtClaimSetBuilder.claims(claims ->
userAttributes.entrySet().stream()
.forEach(entry -> claims.put(entry.getKey(), entry.getValue()))
);
}
}
Now I am assuming that the Resource Server is also configure for multitenancy. Here is the link Spring Security Resource Server Multitenancy. Basically You have to configure two beans for multitenancy like the following
public class OAuth2ResourceServerConfiguration {
....
#Bean
public JWTProcessor<SecurityContext> jwtProcessor(JWTClaimsSetAwareJWSKeySelector<SecurityContext> keySelector) {
ConfigurableJWTProcessor<SecurityContext> jwtProcessor = new DefaultJWTProcessor<>();
jwtProcessor.setJWTClaimsSetAwareJWSKeySelector(keySelector);
return jwtProcessor;
}
#Bean
public JwtDecoder jwtDecoder(JWTProcessor<SecurityContext> jwtProcessor, OAuth2TokenValidator<Jwt> jwtValidator) {
NimbusJwtDecoder decoder = new NimbusJwtDecoder(jwtProcessor);
OAuth2TokenValidator<Jwt> validator = new DelegatingOAuth2TokenValidator<>(JwtValidators.createDefault(), jwtValidator);
decoder.setJwtValidator(validator);
return decoder;
}
}
Now two classes for spring. From which you can get the tenant Identifier from your token.
#Component
public class TenantJwtIssuerValidator implements OAuth2TokenValidator<Jwt> {
private final TenantDataSourceRepository tenantDataSourceRepository;
private final Map<String, JwtIssuerValidator> validators = new ConcurrentHashMap<>();
....
#Override
public OAuth2TokenValidatorResult validate(Jwt token) {
String issuerURL = toTenant(token);
JwtIssuerValidator jwtIssuerValidator = validators.computeIfAbsent(issuerURL, this::fromTenant);
OAuth2TokenValidatorResult oauth2TokenValidatorResult = jwtIssuerValidator.validate(token);
String tenantDatabaseName = JwtService.getTenantDatabaseName(token);
TenantDBContextHolder.setCurrentDb(tenantDatabaseName);
return oauth2TokenValidatorResult;
}
private String toTenant(Jwt jwt) {
return jwt.getIssuer().toString();
}
private JwtIssuerValidator fromTenant(String tenant) {
String issuerURL = tenant;
String tenantDatabaseName = JwtService.getTenantDatabaseName(issuerURL);
TenantDataSource tenantDataSource = tenantDataSourceRepository.findByDatabaseName(tenantDatabaseName);
if (tenantDataSource == null) {
throw new IllegalArgumentException("unknown tenant");
}
JwtIssuerValidator jwtIssuerValidator = new JwtIssuerValidator(issuerURL);
return jwtIssuerValidator;
}
}
Similarly
#Component
public class TenantJWSKeySelector implements JWTClaimsSetAwareJWSKeySelector<SecurityContext> {
....
#Override
public List<? extends Key> selectKeys(JWSHeader jwsHeader, JWTClaimsSet jwtClaimsSet, SecurityContext securityContext) throws KeySourceException {
String tenant = toTenantDatabaseName(jwtClaimsSet);
JWSKeySelector<SecurityContext> jwtKeySelector = selectors.computeIfAbsent(tenant, this::fromTenant);
List<? extends Key> jwsKeys = jwtKeySelector.selectJWSKeys(jwsHeader, securityContext);
return jwsKeys;
}
private String toTenantDatabaseName(JWTClaimsSet claimSet) {
String issuerURL = (String) claimSet.getClaim("iss");
String tenantDatabaseName = JwtService.getTenantDatabaseName(issuerURL);
return tenantDatabaseName;
}
private JWSKeySelector<SecurityContext> fromTenant(String tenant) {
TenantDataSource tenantDataSource = tenantDataSourceRepository.findByDatabaseName(tenant);
if (tenantDataSource == null) {
throw new IllegalArgumentException("unknown tenant");
}
JWSKeySelector<SecurityContext> jwtKeySelector = fromUri(jwkSetUri);
return jwtKeySelector;
}
private JWSKeySelector<SecurityContext> fromUri(String uri) {
try {
return JWSAlgorithmFamilyJWSKeySelector.fromJWKSetURL(new URL(uri));
} catch (Exception ex) {
throw new IllegalArgumentException(ex);
}
}
}
Now what about authorization code grant type grant type flow. I get the tenant identifier in this case too. But when it redirects me to login page then I lost the tenant identifier because I think it creates a new request for the login page from the authorization code request. Anyways I am not sure about it because I have to look into the code of authorization code flow that what it is actually doing. So my tenant identifier is losing when it redirects me to login page.
But in case of password grant type and client credentials grant type there is no redirection so I get the tenant identifier in later stages and I can successfully use it to put into my token claims.
Then on the resource server I get the issuer url. Get the tenant identifier from the issuer url. Verify it. And it connects to the tenant database on resource server.
How I tested it. I used the spring client. You can customize the request for authorization code flow. Password and client credentials to include the custom parameters.
Thanks.
------------------ Solve the Authorization Code login problem for multitenancy -------------
I solved this issue too. Actually what I did in my security configuration. I used the following configuration
public class SecurityConfiguration {
.....
#Bean(name = "authenticationManager")
public AuthenticationManager authenticationManager(AuthenticationManagerBuilder builder) throws Exception {
AuthenticationManager authenticationManager = builder.getObject();
return authenticationManager;
}
#Bean
#DependsOn(value = {"authenticationManager"})
public TenantUsernamePasswordAuthenticationFilter tenantAuthenticationFilter(AuthenticationManagerBuilder builder) throws Exception {
TenantUsernamePasswordAuthenticationFilter filter = new TenantUsernamePasswordAuthenticationFilter();
filter.setAuthenticationManager(authenticationManager(builder));
filter.setAuthenticationDetailsSource(new TenantWebAuthenticationDetailsSource());
//filter.setAuthenticationFailureHandler(failureHandler());
return filter;
}
#Bean
public SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {
FederatedIdentityConfigurer federatedIdentityConfigurer = new FederatedIdentityConfigurer().oauth2UserHandler(new UserRepositoryOAuth2UserHandler());
AuthenticationManagerBuilder authenticationManagerBuilder = http.getSharedObject(AuthenticationManagerBuilder.class);
http.addFilterBefore(tenantAuthenticationFilter(authenticationManagerBuilder), UsernamePasswordAuthenticationFilter.class)
.authorizeRequests(authorizeRequests -> authorizeRequests.requestMatchers(new AntPathRequestMatcher("/h2-console/**")).permitAll()
.antMatchers("/resources/**", "/static/**", "/webjars/**").permitAll()
.antMatchers("/login").permitAll()
.anyRequest().authenticated()
)
......
.apply(federatedIdentityConfigurer);
return http.build();
}
Actually the problem was in case of Authorization Code is that you first redirect to login page. After successfully login you see the consent page. But when you comes to consent page then you lost the tenant parameter.
The reason is the spring internal class OAuth2AuthorizationEndpointFilter intercepts the request for Authorization Code. It checks user is authenticated or not. If user is not authenticated then it shows the login page. After successfully login it checks if consent is required. And if required then it makes a redirect uri with just three parameters. Here is the spring internal code
private void sendAuthorizationConsent(HttpServletRequest request, HttpServletResponse response,
OAuth2AuthorizationCodeRequestAuthenticationToken authorizationCodeRequestAuthentication,
OAuth2AuthorizationConsentAuthenticationToken authorizationConsentAuthentication) throws IOException {
....
if (hasConsentUri()) {
String redirectUri = UriComponentsBuilder.fromUriString(resolveConsentUri(request))
.queryParam(OAuth2ParameterNames.SCOPE, String.join(" ", requestedScopes))
.queryParam(OAuth2ParameterNames.CLIENT_ID, clientId)
.queryParam(OAuth2ParameterNames.STATE, state)
.toUriString();
this.redirectStrategy.sendRedirect(request, response, redirectUri);
} else {
if (this.logger.isTraceEnabled()) {
this.logger.trace("Displaying generated consent screen");
}
DefaultConsentPage.displayConsent(request, response, clientId, principal, requestedScopes, authorizedScopes, state);
}
}
See the above method is private and I found no way that I can customize it. May be there is but I didn't find it. Anyways now your consent controller is call. But there is no tenant Identifier. You can't get it. And after consent there is no way that it connects to tenant database base in identifier.
So the first step is to add tenant identifier to login page. And then after login you should have this tenant identifier so you can set it on your consent page. And after that when you submit your consent form then this parameter will be there.
Btw I did it some time ago and may be I miss something but this is what I did.
Now how you get your parameter at login page. I solved it using the following. First I created a constant as I have to access the name from multiple times
public interface Constant {
String TENANT_DATABASE_NAME = "tenantDatabaseName";
}
Create the following class
public class RedirectModel {
#NotBlank
private String tenantDatabaseName;
public void setTenantDatabaseName(String tenantDatabaseName) {
this.tenantDatabaseName = tenantDatabaseName;
}
public String getTenantDatabaseName() {
return tenantDatabaseName;
}
}
Then on my Login controller I get it using the following code
#Controller
public class LoginController {
#GetMapping("/login")
public String login(#Valid #ModelAttribute RedirectModel redirectModel, Model model, BindingResult result) {
if (!result.hasErrors()) {
String tenantDatabaseName = redirectModel.getTenantDatabaseName();
String currentDb = TenantDBContextHolder.getCurrentDb();
LOGGER.info("Current database is {}", currentDb);
LOGGER.info("Putting {} as tenant database name in model. So it can be set as a hidden form element ", tenantDatabaseName);
model.addAttribute(Constant.TENANT_DATABASE_NAME, tenantDatabaseName);
}
return "login";
}
}
So this is the first step that I have my tenant identifier in my login page that is send to me by request.
Now the configuration that I used in my Security configuration. You can see that I am using TenantUsernamePasswordAuthenticationFilter. Here is the filer
public class TenantUsernamePasswordAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
private static final Logger LOGGER = LogManager.getLogger();
#Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
throws AuthenticationException {
String tenantDatabaseName = obtainTenantDatabaseName(request);
LOGGER.info("tenantDatabaseName is {}", tenantDatabaseName);
LOGGER.info("Setting {} as tenant database name in thread local context.", tenantDatabaseName);
TenantDBContextHolder.setCurrentDb(tenantDatabaseName);
return super.attemptAuthentication(request, response);
}
private String obtainTenantDatabaseName(HttpServletRequest request) {
return request.getParameter(Constant.TENANT_DATABASE_NAME);
}
}
And in the configuration I am setting TenantWebAuthenticationDetailsSource on this filter which is here
public class TenantWebAuthenticationDetailsSource extends WebAuthenticationDetailsSource {
#Override
public TenantWebAuthenicationDetails buildDetails(HttpServletRequest context) {
return new TenantWebAuthenicationDetails(context);
}
}
Here is the class
public class TenantWebAuthenicationDetails extends WebAuthenticationDetails {
private static final long serialVersionUID = 1L;
private String tenantDatabaseName;
public TenantWebAuthenicationDetails(HttpServletRequest request) {
super(request);
this.tenantDatabaseName = request.getParameter(Constant.TENANT_DATABASE_NAME);
}
public TenantWebAuthenicationDetails(String remoteAddress, String sessionId, String tenantDatabaseName) {
super(remoteAddress, sessionId);
this.tenantDatabaseName = tenantDatabaseName;
}
public String getTenantDatabaseName() {
return tenantDatabaseName;
}
}
Now after spring authenticates the user then I have the tenant name in details. Then in the consent controller I use
#Controller
public class AuthorizationConsentController {
....
#GetMapping(value = "/oauth2/consent")
public String consent(Authentication authentication, Principal principal, Model model,
#RequestParam(OAuth2ParameterNames.CLIENT_ID) String clientId,
#RequestParam(OAuth2ParameterNames.SCOPE) String scope,
#RequestParam(OAuth2ParameterNames.STATE) String state) {
......
String registeredClientName = registeredClient.getClientName();
Object webAuthenticationDetails = authentication.getDetails();
if (webAuthenticationDetails instanceof TenantWebAuthenicationDetails) {
TenantWebAuthenicationDetails tenantAuthenticationDetails = (TenantWebAuthenicationDetails)webAuthenticationDetails;
String tenantDatabaseName = tenantAuthenticationDetails.getTenantDatabaseName();
model.addAttribute(Constant.TENANT_DATABASE_NAME, tenantDatabaseName);
}
model.addAttribute("clientId", clientId);
.....
return "consent-customized";
}
}
Now I have my tenant identifier on my consent page. After submitting it it's in the request parameter.
There is another class that I used and it was
public class TenantLoginUrlAuthenticationEntryPoint extends LoginUrlAuthenticationEntryPoint {
public TenantLoginUrlAuthenticationEntryPoint(String loginFormUrl) {
super(loginFormUrl);
}
#Override
protected String determineUrlToUseForThisRequest(HttpServletRequest request, HttpServletResponse response,
AuthenticationException exception) {
String tenantDatabaseNameParamValue = request.getParameter(Constant.TENANT_DATABASE_NAME);
String redirect = super.determineUrlToUseForThisRequest(request, response, exception);
String url = UriComponentsBuilder.fromPath(redirect).queryParam(Constant.TENANT_DATABASE_NAME, tenantDatabaseNameParamValue).toUriString();
return url;
}
}
Anyways this is how I solved it. I don't have any such requirement in any of my project but I want to do it using this new server so I just solved it in this way.
Anyways there is lot of code. I tested it using the Spring oauth2 client and it was working. Hopefully I will create some project and upload it on my Github. Once I will run it again then I will put more explanation here of the flow. Specially for the last part that after submitting the consent how it set in the Thread Local variable.
After that everything is straight forward.
Hopefully it will help.
Thanks

Spring Boot Callback after client receives resource?

I'm creating an endpoint using Spring Boot which executes a combination of system commands (java.lang.Runtime API) to generate a zip file to return to the client upon request, here's the code.
#GetMapping(value = "generateZipFile")
public ResponseEntity<Resource> generateZipFile(#RequestParam("id") Integer id) throws IOException {
org.springframework.core.io.Resource resource = null;
//generate zip file using commandline
resource = service.generateTmpResource(id);
return ResponseEntity.ok()
.header(HttpHeaders.CONTENT_TYPE, "application/zip")
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"randomFile.zip\"")
.body(resource);
//somehow delete generated file here after client receives it
}
I cannot keep stacking up the files on the server for obvious disk limit reasons, so I'm looking for a way to delete the files as soon as the client downloads them. Is there a solution in Spring Boot for this? I basically need to hook a callback that would do the cleanup after the user receives the resource.
I'm using Spring Boot 2.0.6
You can create a new thread but a best solution would be create a ThreadPoolExecutor in order to manage threads or also Scheduled annotation helps us.
new Thread() {
#Override
public void run() {
service.cleanup(id);
}
}.start();
UPDATED
A best answer, it would be using a Stack combine with Thread.
Here is the solution that I've done.
https://github.com/jjohxx/example-thread
I ended up using a HandlerInterceptorAdapter, afterCompletion was the callback I needed. The only challenge I had to deal with was passing through the id of the resource to cleanup, which I handled by adding a header in my controller method:
#GetMapping(value = "generateZipFile")
public ResponseEntity<Resource> genereateZipFile(#RequestParam("id") Integer id,
RedirectAttributes redirectAttributes) throws IOException {
org.springframework.core.io.Resource resource = myService.generateTmpResource(id);;
return ResponseEntity.ok()
.header(HttpHeaders.CONTENT_TYPE, "application/zip")
.header(MyInterceptor.TMP_RESOURCE_ID_HEADER, id.toString())
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"someFile.zip\"")
.body(resource);
}
The interceptor code:
#Component
public class MyInterceptor extends HandlerInterceptorAdapter {
public static final String TMP_RESOURCE_ID_HEADER = "Tmp-ID";
#Autowired
private MyService myService;
#Override
public void afterCompletion(HttpServletRequest request,
HttpServletResponse response,
Object handler,
Exception ex) {
if(response == null || !response.containsHeader(TMP_RESOURCE_ID_HEADER)) return;
String tmpFileId = response.getHeader(TMP_RESOURCE_ID_HEADER);
myService.cleanup(tmpFileId);
}
}
For more information about interceptors see here.

Spring Session Redis and Spring Security how to update user session?

I am building a spring REST web application using spring boot, spring secuirity, and spring session (redis). I am building a cloud application following the gateway pattern using spring cloud and zuul proxy. Within this pattern I am using spring session to manage the HttpSesssion in redis and using that to authorize requests on my resource servers. When an operation is executed that alters the session's authorities, I would like to update that object so that the user does not have to log out to have the updates reflected. Does anyone have a solution for this?
To update the authorities you need to modify the authentication object in two places. One in the Security Context and the other in the Request Context. Your principal object will be org.springframework.security.core.userdetails.User or extend that class (if you have overridden UserDetailsService). This works for modifying the current user.
Authentication newAuth = new UsernamePasswordAuthenticationToken({YourPrincipalObject},null,List<? extends GrantedAuthority>)
SecurityContextHolder.getContext().setAuthentication(newAuth);
RequestContextHolder.currentRequestAttributes().setAttribute("SPRING_SECURITY_CONTEXT", newAuth, RequestAttributes.SCOPE_GLOBAL_SESSION);
To update the session using spring session for any logged in user requires a custom filter. The filter stores a set of sessions that have been modified by some process. A messaging system updates that value when new sessions need to be modified. When a request has a matching session key, the filter looks up the user in the database to fetch the updates. Then it updates the "SPRING_SECURITY_CONTEXT" property on the session and updates the Authentication in the SecurityContextHolder. The user does not need to log out. When specifying the order of your filter it is important that it comes after SpringSessionRepositoryFilter. That object has an #Order of -2147483598 so I just altered my filter by one to make sure it is the next one that is executed.
The workflow looks like:
Modify User A Authority
Send Message To Filter
Add User A Session Keys to Set (In the filter)
Next time User A passed through the filter, update their session
#Component
#Order(UpdateAuthFilter.ORDER_AFTER_SPRING_SESSION)
public class UpdateAuthFilter extends OncePerRequestFilter
{
public static final int ORDER_AFTER_SPRING_SESSION = -2147483597;
private Logger log = LoggerFactory.getLogger(this.getClass());
private Set<String> permissionsToUpdate = new HashSet<>();
#Autowired
private UserJPARepository userJPARepository;
private void modifySessionSet(String sessionKey, boolean add)
{
if (add) {
permissionsToUpdate.add(sessionKey);
} else {
permissionsToUpdate.remove(sessionKey);
}
}
public void addUserSessionsToSet(UpdateUserSessionMessage updateUserSessionMessage)
{
log.info("UPDATE_USER_SESSION - {} - received", updateUserSessionMessage.getUuid().toString());
updateUserSessionMessage.getSessionKeys().forEach(sessionKey -> modifySessionSet(sessionKey, true));
//clear keys for sessions not in redis
log.info("UPDATE_USER_SESSION - {} - success", updateUserSessionMessage.getUuid().toString());
}
#Override
public void destroy()
{
}
#Override
protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException
{
HttpSession session = httpServletRequest.getSession();
if (session != null)
{
String sessionId = session.getId();
if (permissionsToUpdate.contains(sessionId))
{
try
{
SecurityContextImpl securityContextImpl = (SecurityContextImpl) session.getAttribute("SPRING_SECURITY_CONTEXT");
if (securityContextImpl != null)
{
Authentication auth = securityContextImpl.getAuthentication();
Optional<User> user = auth != null
? userJPARepository.findByUsername(auth.getName())
: Optional.empty();
if (user.isPresent())
{
user.get().getAccessControls().forEach(ac -> ac.setUsers(null));
MyCustomUser myCustomUser = new MyCustomUser (user.get().getUsername(),
user.get().getPassword(),
user.get().getAccessControls(),
user.get().getOrganization().getId());
final Authentication newAuth = new UsernamePasswordAuthenticationToken(myCustomUser ,
null,
user.get().getAccessControls());
SecurityContextHolder.getContext().setAuthentication(newAuth);
session.setAttribute("SPRING_SECURITY_CONTEXT", newAuth);
}
else
{
//invalidate the session if the user could not be found
session.invalidate();
}
}
else
{
//invalidate the session if the user could not be found
session.invalidate();
}
}
finally
{
modifySessionSet(sessionId, false);
}
}
}
filterChain.doFilter(httpServletRequest, httpServletResponse);
}

Authorization in jersey framework

I am using jersey (java) framework. I did authentication based on cookie using Container request filter. Now I have to do Authorization. So, how to I proceed? Quick guidance please.
Jersey has #RolesAllowed("role") annotation to facilitate auth check. Make use of:
#Context
HttpServletRequest httpRequest;`
and in the login method put identity into session like here:
HttpSession session = httpRequest.getSession(true);
session.setAttribute(key, val);
in filter
final String name = session.getAttribute(key);
...
SecurityContext securityContext = new SecurityContext() {
public boolean isUserInRole(String roleName) {
return roleName.equals("role");
}
...
public Principal getUserPrincipal() {
...
return new Principal() {
public String getName() {
return name;
}
};
...
}
...
};
requestContext.setSecurityContext(securityContext);
That's it in short. It is quite common approach. If you want I can share ref impl on GitHub.

Configure two cxf jaxrs clients to use the same session (cookies)

I want to connect to a REST server with a jaxrs client using apache cxf. The server has an url to authenticate and some other urls to do the actual stuff. After the login the server creates a session and keeps the connection open for 30 min. My problem is that the client doesn't store the cookies and I always get a new (not authenticated) session on the server.
I configured the clients in my spring application context.
<jaxrs:client id="loginResource"
serviceClass="com.mycompany.rest.resources.LoginResource"
address="${fsi.application.url}">
</jaxrs:client>
<jaxrs:client id="actionResource"
serviceClass="com.mycompany.rest.resources.ActionResource"
address="${fsi.application.url}">
</jaxrs:client>
How can I configure both clients to use the same session or share the session-cookie between the clients?
I have been struggling with the same problem, and I just finally arrived at a solution.
1) Make the client retain cookies.
WebClient.getConfig(proxy).getRequestContext().put(
org.apache.cxf.message.Message.MAINTAIN_SESSION, Boolean.TRUE);
Perhaps there's a way to accomplish the above via configuration vs. programmatically.
2) Copy the cookies from one client to the other.
public static void copyCookies(Object sourceProxy, Object targetProxy) {
HTTPConduit sourceConduit = WebClient.getConfig(sourceProxy).getHttpConduit();
HTTPConduit targetConduit = WebClient.getConfig(targetProxy).getHttpConduit();
targetConduit.getCookies().putAll(sourceConduit.getCookies());
}
After using proxy A to authenticate, I call the above method to share its cookies with proxy B, which does the actual work.
I use the following request/response filter to pass the session-id in each request:
ClientResponseFilter >> used to extract the session-id from session-cookie
ClientRequestFilter >> pass the session-id as cookie header in each request
private class SessionIdRequestFilter implements ClientRequestFilter, ClientResponseFilter {
/** cookie header key */
private static final String COOKIE_KEY = "Cookie"; //$NON-NLS-1$
/** name of the session cookie */
private String cookieName = "JSESSIONID";
/** holds the session id from the cookie */
private String sessionId;
#Override
public void filter(ClientRequestContext request) throws IOException {
// append session cookie to request header
if (!request.getHeaders().containsKey(COOKIE_KEY) && sessionId != null) {
request.getHeaders().add(COOKIE_KEY, getCookieName() + "=" + sessionId); //$NON-NLS-1$
}
}
#Override
public void filter(ClientRequestContext request, ClientResponseContext response) throws IOException {
// find sessionId only if not already set
if (sessionId == null) {
Map<String, NewCookie> cookies = response.getCookies();
if (cookies != null && !cookies.isEmpty()) {
NewCookie cookie = cookies.get(getCookieName());
if (cookie != null) {
sessionId = cookie.getValue();
}
}
}
}
private String getCookieName() {
return cookieName;
}
}
To register the filter use:
RestClientBuilder.newBuilder().baseUri(apiUri).register(new SessionIdRequestFilter());

Resources