Kotlin Spring Session concurrent session control on already defined Security config - spring

This is a repost from the Github issues page.
Confused on why there are no behavior changes when adding the concurrency session control with Spring session with this documentation
Versions:
Spring Security 5.2.1.RELEASE
Spring Session-JDBC 2.2.0.RELEASE
Spring Boot 2.2.4.RELEASE
Code snippets from our code:
SecurityConfig.kt
#EnableWebSecurity
#Profile("auth")
#Configuration
class SecurityConfig<S: Session> : WebSecurityConfigurerAdapter() {
//<some-code-here>
#Autowired
private lateinit var sessionRepository: FindByIndexNameSessionRepository<S>
override fun configure(http: HttpSecurity) {
/*
TODO enforce requiring application/json content type everywhere (apparently except file upload)
*/
val publicPaths = arrayOf(
"/api/v1/centralPortal/current/forgot-password",
"/api/v1/centralPortal/current/reset-password/*",
"/api/v1/centralPortal/current/onboard/*",
"/login",
"/api/v1/public/**" // CSP reporting is there, needs CSRF disabled
)
val filter = JsonLoginFilter(objectMapper, audit, rateLimiter, userManagementService, getAuthMode(env))
filter.rateLimitPPS = rateLimitPermits / rateLimitSeconds
filter.setSessionAuthenticationStrategy(sas)
filter.setAuthenticationManager(authenticationManager())
filter.setAuthenticationSuccessHandler { request, response, _ ->
val csrfToken = request.getAttribute(CsrfToken::class.java.name) as CsrfToken
response.addHeader(csrfToken.headerName, csrfToken.token)
}
filter.setAuthenticationFailureHandler { request, response, exception ->
excHandlerAdvice.handleUnauthenticated(request, response, exception, filter.obtainRetries()) }
http
.cors().and()
.httpBasic().disable()
.formLogin().disable()
.rememberMe().disable()
.headers()
// do custom handling of the X-Frame-Options header because some pages need to be iframed
.frameOptions().disable()
.addHeaderWriter(ConfigurableFrameOptionsHeaderWriter("/assets/pdfjs/web/viewer.html", "/ws/frame.html"))
.and()
.csrf()
.ignoringAntMatchers(*publicPaths)
.and()
.authorizeRequests()
.antMatchers("/manifest.json", "/manifest.webmanifest").permitAll()
.antMatchers("/", "/index.js", "/main.js", "/vendor.js", "/service-worker.js", "/workbox-v*/workbox-sw.js", "/precache.*.js", "/*.ico", "/ui/**", "/index.html").permitAll()
.regexMatchers("^/\\w\\w/ui/.*$").permitAll()
.antMatchers("/assets/**/*.webp", "/assets/**/*.png", "/assets/**/*.jpg", "/assets/**/*.svg", "/assets/**/*.gif").permitAll()
.antMatchers("/assets/**/*.woff", "/assets/**/*.woff2", "/assets/**/*.ttf", "/assets/**/*.eot").permitAll()
.antMatchers("/assets/**/*.css").permitAll()
.antMatchers(*publicPaths).permitAll()
.antMatchers("/img/logo.png").permitAll()
.anyRequest().authenticated()
.and()
.exceptionHandling()
.accessDeniedHandler { request, response, accessDeniedException -> excHandlerAdvice.handleAccessDenied(request, response, accessDeniedException) }
.authenticationEntryPoint { request, response, authException -> excHandlerAdvice.handleUnauthenticated(request, response, authException) }
.and()
.addFilter(filter)
.logout()
.logoutSuccessHandler { request, response, auth -> Unit } // don't redirect
.and()
http.headers()
.cacheControl().disable()
.addHeaderWriter(CacheControlWriterWithWorkaround())
// Concurrency control code
http.sessionManagement()
.maximumSessions(1)
.maxSessionsPreventsLogin(true)
.sessionRegistry(sessionRegistry())
}
private fun sessionRegistry(): SpringSessionBackedSessionRegistry<S> {
return SpringSessionBackedSessionRegistry(this.sessionRepository)
}
}
JsonLoginFilter.kt
data class UserLogin(val username: String, val password: String)
class JsonLoginFilter(private val objectMapper: ObjectMapper, val audit: AuditService?, val rateLimiter: RateLimiterAspect, val userManagementService: UserManagementService?, val authMode: AuthMode) : UsernamePasswordAuthenticationFilter() {
var rateLimitPPS: Double = 1 / 2.0 // rate limiter permits per second default value
private var _parsed: UserLogin? = null
private val log = loggerFor<JsonLoginFilter>()
fun parsedLogin(request: HttpServletRequest): UserLogin {
val body = request.inputStream
return objectMapper.readValue(body, UserLogin::class.java)
}
private fun loginAttempt(success: Boolean) {
log.info("Login attempt username=${_parsed?.username} success=$success")
if (success) {
resetRetries()
}
audit?.eventWithPrincipal(
AuditObjectCategory.USERS,
"User",
_parsed?.username,
if (success) OtherActions.USER_LOGIN else OtherActions.USER_LOGIN_FAIL,
_parsed?.username
)
}
override fun attemptAuthentication(request: HttpServletRequest, response: HttpServletResponse?): Authentication {
try {
rateLimiter.rateLimit(request, "login", rateLimitPPS)
_parsed = parsedLogin(request)
val authResult = super.attemptAuthentication(request, response)
loginAttempt(authResult != null)
return authResult
} catch (e: RateLimiterException) {
throw e
} catch (ex: AuthenticationException) {
loginAttempt(false)
if (authMode == AuthMode.BUILTIN) userManagementService?.checkUserRetries(_parsed!!.username)
throw ex
}
}
fun obtainRetries(): Int? {
return if (authMode == AuthMode.BUILTIN) userManagementService?.getUserRetries(_parsed!!.username) else 0
}
fun resetRetries() {
if (authMode == AuthMode.BUILTIN) {
userManagementService?.resetUserRetries(_parsed!!.username)
}
}
override fun obtainPassword(request: HttpServletRequest): String {
return _parsed!!.password
}
override fun obtainUsername(request: HttpServletRequest): String {
return _parsed!!.username
}
}
spring.session.store-type=jdbc
So my expected behavior based on the code and the documentation would be when there is current login user then another user logs in into another browser, it should prevent the login with maxSessionPreventsLogin(true).
So the actual behavior on the code above is that 2 browser with the same user can login, which I didn't expect, since I followed the docs. I'm not really sure if I am missing something, as this is my first time I am encountering Spring Session and Spring Security. Please also take note that the sessions are stored in a DB rather than in-memory.
I would really love if someone can point me to the steps of the correct Spring session configuration, or maybe the documentation should have a precursor on what to configure before doing this, or what when is not an applicable approach.
Thanks

Related

Query a password protected WebService in Spring-Boot via HealthIndicator

I need to query a WebService and check its results to calculate a Health.status() in SpringBoot.
Health is then used for k8s in its liveness probe: /actuator/health.
However, the WebService that I need to query needs a username and password, whereas the endpoint itself does not, as it is consumed by k8s and Prometheus. Please see the following configuration:
http
.authorizeRequests()
.requestMatchers(EndpointRequest.to("info", "health")).permitAll()
.anyRequest().authenticated()
.and()
.httpBasic()
.and()
.sessionManagement().sessionCreationPolicy(STATELESS)
.and()
.csrf().disable()
As the WebService in question can be queried when httpBasic() is used to authenticate, I need to somehow use basic authentication in HealthIndicator without passing these values in the request.
What I want to do is to have "health" permitted for unauthorised requests, but still transparently authenticate with a technical user to query a third party service. Please see comment in following snippet:
override fun health(): Health {
val status = Health.up()
val beforeServiceCall = System.nanoTime()
val partner =
try {
// COMMENT FOR StackOverflow
// this call here needs authentication resp. a token which is only generated when basicAuth is used
partnerService.getPartner(
process = metricsProperties.process,
partnerNumber = metricsProperties.partnernumber
)
} catch (e: Exception) {
null
}
val afterServiceCall = System.nanoTime()
return status
.withDetail(PARTNER_FOUND, partner.let {
if (it != null) {
1.0
} else {
status.unknown()
0.0
}
})
.withDetail(PARTNER_QUERY_TIME_MS, ((afterServiceCall - beforeServiceCall) / 1e6).toDouble())
.build()
tl;dr;
I need to somehow trigger the WebSecurityConfigurerAdapter's configure method from within my HealthIndicator to obtain a valid token to be able to query a third party service. How can this configure method be triggered explicitly?
#Configuration
#EnableWebSecurity
class SecurityConfig(omitted params) :
WebSecurityConfigurerAdapter() {
override fun configure(auth: AuthenticationManagerBuilder) {
auth
.authenticationProvider(object : AuthenticationProvider {
override fun authenticate(authentication: Authentication): Authentication? {
val authorizationGrant = ResourceOwnerPasswordCredentialsGrant(authentication.name,
Secret(authentication.credentials as String))
val tokenRequest = TokenRequest(URI(keycloakUrl),
ClientID(clientID),
authorizationGrant)
val tokenResponse = TokenResponse
.parse(tokenRequest
.toHTTPRequest()
.send())
if (tokenResponse.indicatesSuccess()) {
return UsernamePasswordAuthenticationToken(
authentication.name,
null,
emptyList())
} else {
throw BadCredentialsException("Error logging in")
}
}
override fun supports(authentication: Class<*>): Boolean {
return authentication == UsernamePasswordAuthenticationToken::class.java
}
})
.eraseCredentials(false)
}

Spring Webflux: Read request-body in ServerWebExchangeMatcher

I have to create an application with just one endpoint where users can log in and do their other operational stuff. Users should be able to log in with a request body like:
{
"login": {
"token": "12345"
}
}
So every request with a body like this should be allowed via .permitAll() by the WebFilterChain.
The chain is configured like this:
#Bean
public SecurityWebFilterChain chain(ServerHttpSecurity http) {
return http.authorizeExchange(exchanges -> {
exchanges.matchers(new LoginRequestMatcher())
.permitAll()
.pathMatchers("api/v1*")
.hasRole("USER");
})
.csrf()
.disable()
.build();
}
The LoginRequestMatcher analyzes the body of the request and returns a Match if the pattern is ok. So far, this works quite fine.
My problem is that later in the Controller, the request body can not be accessed any more, so I'm currently trying to put it into a cache or the context to be able to access it later on.
Here is my current implementation of the LoginRequestMatcher:
public class LoginRequestMatcher implements ServerWebExchangeMatcher {
private static final Pattern loginRequestPattern = Pattern.compile("...loginPattern");
#Override
public Mono<MatchResult> matches(final ServerWebExchange exchange) {
return exchange.getRequest().getBody()
.map(dataBuffer -> {
byte[] byteArray=new byte[dataBuffer.readableByteCount()];
dataBuffer.read(byteArray);
DataBufferUtils.release(dataBuffer);
return byteArray;
}).defaultIfEmpty(new byte[0])
.map(bytes -> {
ServerHttpRequestDecorator decorator = initDecorator(exchange, bytes);
exchange.mutate().request(decorator).build();
Mono.just(decorator).contextWrite(ctx -> ctx.put("decorator",decorator))
return decorator;
}
)
// Rest of the code checks for Regex in the body and works fine.
.next();
Now, when I try to get the cached request-body after the RequestMatcher is processed (f.e. in a WebFilter), the context does not contain an object with the key "decorator".
#Configuration
#ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.REACTIVE)
public class HttpRequestBodyCachingFilter implements WebFilter {
#Override
public Mono<Void> filter(final ServerWebExchange exchange, final WebFilterChain chain) {
final HttpMethod method = exchange.getRequest().getMethod();
// Nothing to cache for GET and DELETE
if (method==null || HttpMethod.GET==method || HttpMethod.DELETE==method) {
return chain.filter(exchange);
}
// Get decorator from context
return Mono.just(exchange)
//ctx.get("decorator") throws Exception here, because the key does not exist.
.flatMap(s -> Mono.deferContextual(ctx -> ctx.get("decorator")))
.map(decorator ->
(ServerHttpRequestDecorator) decorator)
.flatMap(decorator -> chain.filter(exchange.mutate().request(decorator).build()));
}
}
How can I make the body readable after accessing it in inside a ServerWebExchangeMatcher?

Webflux JWT Authorization not working fine

I am following a tutorial about JWT in a spring reactive context (webflux).
The token generation is working fine, however the authorization is not working when I use the Authorization with bearer
Here is what I have done:
#EnableWebFluxSecurity
#EnableReactiveMethodSecurity
public class WebSecurityConfig{
#Autowired private JWTReactiveAuthenticationManager authenticationManager;
#Autowired private SecurityContextRepository securityContext;
#Bean public SecurityWebFilterChain configure(ServerHttpSecurity http){
return http.exceptionHandling()
.authenticationEntryPoint((swe , e) -> {
return Mono.fromRunnable(()->{
System.out.println( "authenticationEntryPoint user trying to access unauthorized api end points : "+
swe.getRequest().getRemoteAddress()+
" in "+swe.getRequest().getPath());
swe.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
});
}).accessDeniedHandler((swe, e) -> {
return Mono.fromRunnable(()->{
System.out.println( "accessDeniedHandler user trying to access unauthorized api end points : "+
swe.getPrincipal().block().getName()+
" in "+swe.getRequest().getPath());
swe.getResponse().setStatusCode(HttpStatus.FORBIDDEN);
});
})
.and()
.csrf().disable()
.formLogin().disable()
.httpBasic().disable()
.authenticationManager(authenticationManager)
.securityContextRepository(securityContext)
.authorizeExchange()
.pathMatchers(HttpMethod.OPTIONS).permitAll()
.pathMatchers("/auth/login").permitAll()
.anyExchange().authenticated()
.and()
.build();
}
As you can see, I want to simply deny all not authorized requests other than login or options based ones.
The login is working fine and I'm getting a token.
But trying to logout (a tweak that I implemented my self to make it state-full since I m only learning) is not working.
Here is my logout controller:
#RestController
#RequestMapping(AuthController.AUTH)
public class AuthController {
static final String AUTH = "/auth";
#Autowired
private AuthenticationService authService;
#PostMapping("/login")
public Mono<ResponseEntity<?>> login(#RequestBody AuthRequestParam arp) {
String username = arp.getUsername();
String password = arp.getPassword();
return authService.authenticate(username, password);
}
#PostMapping("/logout")
public Mono<ResponseEntity<?>> logout(#RequestBody LogoutRequestParam lrp) {
String token = lrp.getToken();
return authService.logout(token);
}
}
The logout request is as below:
As stated in images above, I believe that I m doing fine, however I m getting the error log message:
authenticationEntryPoint user trying to access unauthorized api end points : /127.0.0.1:45776 in /auth/logout
Here is my security context content:
/**
* we use this class to handle the bearer token extraction
* and pass it to the JWTReactiveAuthentication manager so in the end
* we produce
*
* simply said we extract the authorization we authenticate and
* depending on our implementation we produce a security context
*/
#Component
public class SecurityContextRepository implements ServerSecurityContextRepository {
#Autowired
private JWTReactiveAuthenticationManager authenticationManager;
#Override
public Mono<SecurityContext> load(ServerWebExchange swe) {
ServerHttpRequest request = swe.getRequest();
String authorizationHeaderContent = request.getHeaders().getFirst(HttpHeaders.AUTHORIZATION);
if( authorizationHeaderContent !=null && !authorizationHeaderContent.isEmpty() && authorizationHeaderContent.startsWith("Bearer ")){
String token = authorizationHeaderContent.substring(7);
Authentication authentication = new UsernamePasswordAuthenticationToken(token, token);
return this.authenticationManager.authenticate(authentication).map((auth) -> {
return new SecurityContextImpl(auth);
});
}
return Mono.empty();
}
#Override
public Mono<Void> save(ServerWebExchange arg0, SecurityContext arg1) {
throw new UnsupportedOperationException("Not supported yet.");
}
}
I'm unable to see or find any issue or error that I have made. Where is the mistake?
There's a difference in writing
//Wrong
Jwts.builder()
.setSubject(username)
.setClaims(claims)
and
//Correct
Jwts.builder()
.setClaims(claims)
.setSubject(username)
Indeed, look at setSubject method in the DefaultJwtBuilder class :
#Override
public JwtBuilder setSubject(String sub) {
if (Strings.hasText(sub)) {
ensureClaims().setSubject(sub);
} else {
if (this.claims != null) {
claims.setSubject(sub);
}
}
return this;
}
When setSubject(username) is called first, ensureClaims() creates a DefaultClaims without yours and if you call setClaims(claims) the precedent subject is lost ! This JWT builder is bogus.
Otherwise, you're importing the wrong Role class in JWTReactiveAuthenticationManager, you have to replace :
import org.springframework.context.support.BeanDefinitionDsl.Role;
by
import com.bridjitlearning.www.jwt.tutorial.domain.Role;
Last and not least, validateToken() will return always false because of the check(token). put call is coming too late, you have to be aware of that. Either you remove this check or you move the put execution before calling the check method.
I'am not sure about what you want to do with resignTokenMemory, so i'll let you fix it by your own:
public Boolean validateToken(String token) {
return !isTokenExpired(token) && resignTokenMemory.check(token);
}
Another thing, your token is valid only 28,8 second, for testing raison i recommend you to expiraiton * 1000.

Spring SSO with facebook filter and JWT

here is my problem.
I got a first authentication with mail and JWT with Spring boot 1.5.3.
=> works perfectly
Then i made a SSO filter to allow facebook tokens
The thing is, on first authentication it's ok. My server get the Token, then check with fb that says ok then it says ok to my client.
After that if i don't encode my token with my JWT token enhancer, my server says that it is not able to decode it as JSON.
Just that i know, i would normally not have to encode myself as it should be done automatically after my chain filter if i say ok ??
This code works but i've done the jwt myself, is that possible i've missed something ????
public class MyOAuth2ClientAuthenticationProcessingFilter extends OAuth2ClientAuthenticationProcessingFilter {
public MyOAuth2ClientAuthenticationProcessingFilter(String defaultFilterProcessesUrl) {
super(defaultFilterProcessesUrl);
}
#Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
throws AuthenticationException, IOException, ServletException {
log.info("[attemptAuthentication facebook]");
Authentication result = null;
try {
String token = request.getHeader("oauth_token");
oauth2ClientContext.setAccessToken(new DefaultOAuth2AccessToken(token));
result = super.attemptAuthentication(request, response);
if(result.isAuthenticated()) {
FacebookService facebookService = new BasicFacebookService(token);
User fbUser = facebookService.getUser();
if(fbUser == null) {
throw new IllegalArgumentException(" fb user cannot be null");
}
if(!userService.isLoginExists(fbUser.getId())) {
CreateSocialUserModel model = new CreateSocialUserModel(
token,
DateUtil.getNow(),
"facebook");
userService.createSocialUser(model, fbUser);
}
//--- Create custom JWT token from facebook token
UserInfoTokenServices tokenService = new UserInfoTokenServices(
"https://graph.facebook.com/me",
facebookProperties.getAppId());
OAuth2AccessToken enhancedToken = jwtTokenEnhancer.enhance(oauth2ClientContext.getAccessToken(),
tokenService.loadAuthentication(oauth2ClientContext.getAccessToken().getValue()));
TokenResponse tokenResponse = new TokenResponse(enhancedToken.getValue(),
enhancedToken.getTokenType(),
enhancedToken.getRefreshToken() != null ? enhancedToken.getRefreshToken().getValue() : "");
ObjectMapper mapper = new ObjectMapper();
String jsonTokenEnhancedJack = mapper.writeValueAsString(tokenResponse);
response.addHeader("Content-Type", "application/json");
response.getWriter().flush();
response.getWriter().print(jsonTokenEnhancedJack);
}
return result;
} catch (Exception e) {
log.info("error");
log.error("error", e);
e.printStackTrace();
} finally {
return result;
}
}
}
Thank you in advance
As asked by Son Goku just putting some code to help him
First you have to put the filter like this
#Override
protected void configure(HttpSecurity http) throws Exception {
http.antMatcher("/**")
.addFilterBefore(ssoFilter(), BasicAuthenticationFilter.class)
.authorizeRequests()
.anyRequest().permitAll()
.and().csrf().disable();
}
private Filter ssoFilter() {
OAuth2ClientAuthenticationProcessingFilter facebookFilter = new MyOAuth2ClientAuthenticationProcessingFilter("/user/social");
OAuth2RestTemplate facebookTemplate = new OAuth2RestTemplate(Oauth2facebook(), oauth2ClientContext);
facebookFilter.setRestTemplate(facebookTemplate);
facebookFilter.setTokenServices(new UserInfoTokenServices(
"https://graph.facebook.com/me",
facebookProperties.getAppId()
));
return facebookFilter;
}
After that you can use the OAuth2ClientAuthenticationProcessingFilter as in first question.
Also, in first question i customized this method a lot, it works but i was surprised to not find this as easy as these library use to be.
Hope it helps you, i have struggled a bit too on this.
Maybe now, spring boot handle this much more easily.

spring 4.1 javaConfg setting to get requestCache working

similar to this:
Spring 3.1: Redirect after login not working
when an authenticated user becomes inauthenticated while deep-linking into a single page web app.
Spring security redirects to logon but:
SavedRequest savedRequest = new HttpSessionRequestCache().getRequest(request, response);
is null
thus i cannot devine the url or params to send re-authenticated user to requested page
#Configuration
#EnableWebSecurity
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf().disable()
.authorizeRequests()
.antMatchers() //redacted .
.antMatchers("/**").permitAll()
.and()
.formLogin()
.loginPage("/x/y/logon")
.usernameParameter("userLogon") //redacted
.loginProcessingUrl("/x/y/logon") //redacted
.defaultSuccessUrl("/x/", true)
.failureUrl("/x/y/logon?error=true")
.and()
.logout()
.logoutUrl("/x/y/logout")
.logoutSuccessUrl("/x/")
.permitAll();
}
}
-- controller --
#RequestMapping(method=RequestMethod.GET, value="/y/logon")
public ModelAndView logonHandler(HttpServletRequest request, HttpServletResponse response) {
List<Client> clients = //manager call for list of clients to show at logon
SavedRequest savedRequest = new HttpSessionRequestCache().getRequest(request, response);
if (savedRequest != null) {
String reqUrl = savedRequest.getRedirectUrl();
String[] urlParams = reqUrl.split("&");
String prefix = "";
String urlParam = "";
String cid = "";
try {
urlParam = urlParams[1];
} catch(IndexOutOfBoundsException ioob) { }
if (reqUrl.contains("cid=")) { cid = reqUrl.substring(reqUrl.indexOf("cid=")+4, reqUrl.indexOf("&")); }
if (reqUrl.contains("?")) { reqUrl = reqUrl.substring(0, reqUrl.indexOf("?")); }
prefix = reqUrl.substring(reqUrl.indexOf("/x/")+6, reqUrl.indexOf("/x/")+8);
reqUrl = reqUrl.substring(reqUrl.indexOf(prefix)+2);
if (reqUrl.contains("/")) {
reqUrl = reqUrl.substring(0, reqUrl.indexOf("/"));
}
request.setAttribute("requestUrl", prefix+reqUrl);
request.setAttribute("urlParam", urlParam);
request.setAttribute("cid", cid);
}
request.setAttribute("IPAddress", request.getRemoteAddr());
return new ModelAndView("x/logon", "clients", clients);
}
problem is, SavedRequest is null
is this an issue with:
alwaysUseDefaultTargetUrl property?
if yes, how in javaConfig does one set this property?
----- on edit to address comments ------
i'll explain my understanding of ea. .formLogon() settings:
logonPage() will be read by spring and control redirect to logon page when you are not authorized (cookie expire/db record del, etc). There are many ways that a session can not be authorized and spring needs to know what page to send unauth requests to. My manual logon handler only handles requests to the logon url.
usernameParameter() is to change from the default form input name thus obfuscating that one is using spring security.
loginProcessingUrl() this seems to conflict with the custom logonHandler, but i think its req to handle the post and allow for spring to create a secure sesson.
defaultSucessUrl() tells spring where to go after successful logon (the post request).
failureUrl() defines the url for failed logon.
nowhere in my custom logon handler for the get request, are those settings in conflict... i think... but i've read the docs and the Spring Security 3 book and lots of online resources and i still do not feel as though i have a solid understanding of spring security... so i may be way off

Resources