Spring Boot - Custom Filter/Stateless auth and #Secured annotation - spring-boot

I have been struggling with this for over 2 hours with no luck after reading around 10 different articles.
I want to use my custom filter to perform stateless authorization based on roles from DB and #Secured annotation.
Let's start with my example account identified in database by api-key: '6c1bb23e-e24c-41a5-8f12-72d3db0a6979'.
He has following String role fetched from DB: 'FREE_USER_ROLE'.
My filter:
public class ApiKeyAuthFilter extends OncePerRequestFilter {
private final AccountService accountService;
private final GlobalExceptionsAdvice exceptionAdvice;
private static final String API_KEY_HEADER_FIELD = "X-AUTH-KEY";
public static final List<String> NON_AUTH_END_POINTS
= Collections.unmodifiableList(Arrays.asList("/Accounts", "/Accounts/Login"));
AntPathMatcher pathMatcher = new AntPathMatcher();
public ApiKeyAuthFilter(AccountService accountService, GlobalExceptionsAdvice exceptionAdvice) {
this.accountService = accountService;
this.exceptionAdvice = exceptionAdvice;
}
#Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain fc) throws ServletException, IOException {
Optional authKey = Optional.ofNullable(request.getHeader(API_KEY_HEADER_FIELD));
if (!authKey.isPresent()) {
sendForbiddenErrorMessage(response);
} else {
try {
AccountDTO account = accountService.findByApiKey(authKey.get().toString());
Set<GrantedAuthority> roles = new HashSet();
account.getRoles().forEach((singleRole) -> roles.add(new SimpleGrantedAuthority(singleRole.getName())));
Authentication accountAuth = new UsernamePasswordAuthenticationToken(account.getEmail(), account.getApiKey(),
roles);
SecurityContextHolder.getContext().setAuthentication(accountAuth);
SecurityContextHolder.getContext().getAuthentication().getAuthorities().forEach((role) -> {
System.out.println(role.getAuthority());
});
fc.doFilter(request, response);
} catch (ElementDoesNotExistException ex) {
//TODO: Add logging that user tried to falsy authenticate
sendForbiddenErrorMessage(response);
}
}
}
#Override
protected boolean shouldNotFilter(HttpServletRequest request) throws ServletException {
return NON_AUTH_END_POINTS.stream().anyMatch(p -> {
return pathMatcher.match(p, request.getServletPath())
&& request.getMethod().equals("POST");
});
}
private void sendForbiddenErrorMessage(HttpServletResponse resp) throws IOException {
ObjectMapper mapper = new ObjectMapper();
ErrorDetail error = exceptionAdvice.handleAccessDeniedException();
resp.setStatus(HttpServletResponse.SC_FORBIDDEN);
resp.setContentType("application/json");
resp.setCharacterEncoding("UTF-8");
resp.getWriter().write(mapper.writeValueAsString(error));
}
As You can see I am using X-AUTH-KEY header to retrieve provided apiKey, then I fetch info from Database based on that key and assign appropiate roles into SecurityContextHolder. Until that point everything works. I am sending poper apiKey, DB returns 'FREE_USER_ROLE'.
My #Configuration annotation class. (I bet something is wrong here but I can not tell what):
#Configuration
#EnableWebSecurity
#EnableGlobalMethodSecurity(securedEnabled = true)
public class ApiKeySecurityConfiguration extends WebSecurityConfigurerAdapter {
AccountService accountService;
GlobalExceptionsAdvice exceptionAdvice;
#Autowired
public ApiKeySecurityConfiguration(AccountService accountService, GlobalExceptionsAdvice exceptionAdvice) {
this.accountService = accountService;
this.exceptionAdvice = exceptionAdvice;
}
#Override
protected void configure(HttpSecurity httpSecurity) throws Exception {
httpSecurity.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
httpSecurity.csrf().disable();
httpSecurity.authorizeRequests().anyRequest().authenticated();
httpSecurity.addFilterBefore(new ApiKeyAuthFilter(accountService, exceptionAdvice), UsernamePasswordAuthenticationFilter.class);
}
}
And final piece of puzzle - Controller that uses #Secured:
#RestController
#RequestMapping("/Accounts")
public class AccountsResource {
#Secured({"FREE_USER_ROLE"})
#PutMapping()
public boolean testMethod() {
return true;
}
}
I have tried with both 'FREE_USER_ROLE' and 'ROLE_FREE_USER_ROLE'. Everytime I get 403 Forbidden.

So I have spent some more time yesterday on that and I have managed to get it working with #PreAuthorize annotation. Posting code below because it may be useful to someone in future.
Filter:
#Component
public class ApiKeyAuthFilter extends OncePerRequestFilter {
private final AccountService accountService;
private final GlobalExceptionsAdvice exceptionAdvice;
private static final String API_KEY_HEADER_FIELD = "X-AUTH-KEY";
public static final List<String> NON_AUTH_END_POINTS
= Collections.unmodifiableList(Arrays.asList("/Accounts", "/Accounts/Login"));
AntPathMatcher pathMatcher = new AntPathMatcher();
#Autowired
public ApiKeyAuthFilter(AccountService accountService, GlobalExceptionsAdvice exceptionAdvice) {
this.accountService = accountService;
this.exceptionAdvice = exceptionAdvice;
}
#Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain fc) throws ServletException, IOException {
Optional authKey = Optional.ofNullable(request.getHeader(API_KEY_HEADER_FIELD));
if (!authKey.isPresent()) {
sendForbiddenErrorMessage(response);
} else {
try {
AccountDTO account = accountService.findByApiKey(authKey.get().toString());
Set<GrantedAuthority> roles = new HashSet();
account.getRoles().forEach((singleRole) -> roles.add(new SimpleGrantedAuthority(singleRole.getName())));
Authentication accountAuth = new UsernamePasswordAuthenticationToken(account.getEmail(), account.getApiKey(),
roles);
SecurityContextHolder.getContext().setAuthentication(accountAuth);
SecurityContextHolder.getContext().getAuthentication().getAuthorities().forEach((role) -> {
System.out.println(role.getAuthority());
});
fc.doFilter(request, response);
} catch (ElementDoesNotExistException ex) {
//TODO: Add logging that user tried to falsy authenticate
sendForbiddenErrorMessage(response);
}
}
}
#Override
protected boolean shouldNotFilter(HttpServletRequest request) throws ServletException {
return NON_AUTH_END_POINTS.stream().anyMatch(p -> {
return pathMatcher.match(p, request.getServletPath())
&& request.getMethod().equals("POST");
});
}
private void sendForbiddenErrorMessage(HttpServletResponse resp) throws IOException {
ObjectMapper mapper = new ObjectMapper();
ErrorDetail error = exceptionAdvice.handleAccessDeniedException();
resp.setStatus(HttpServletResponse.SC_FORBIDDEN);
resp.setContentType("application/json");
resp.setCharacterEncoding("UTF-8");
resp.getWriter().write(mapper.writeValueAsString(error));
}
}
Configuration file:
#Configuration
#EnableWebSecurity
#EnableGlobalMethodSecurity(prePostEnabled = true)
public class ApiKeySecurityConfiguration extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity httpSecurity) throws Exception {
httpSecurity.
csrf().disable().
sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
}
}
Secured methods and methods allowed for anybody to use:
#RestController
#RequestMapping("/Accounts")
public class AccountsResource {
#PostMapping
#PreAuthorize("permitAll()")
public boolean forAll() {
return true;
}
#PutMapping()
#PreAuthorize("hasAuthority('FREE_USER_ROLE')")
public boolean testMethod() {
return true;
}
}

Related

Spring boot SecurityContextHolder.getContext() NPE when using #Async in a #Scheduled service

I am using a scheduled service in spring boot app , i need to get the current connected user inside that service , my problem is that
SecurityContextHolder.getContext().getAuthentication()
returns the current connected user only once ( just after i am logged in ) , but in the next running tasks
SecurityContextHolder.getContext().getAuthentication()
returns NPE , i have searched and found that SecurityContextHolder is not shared outside the main thread.
My Service :
#Service
#EnableScheduling
public class SsePushNotificationService {
public void addEmitter(final SseEmitter emitter) {
emitters.add(emitter);
}
public void removeEmitter(final SseEmitter emitter) {
emitters.remove(emitter);
}
#Async("taskExecutor")
#Scheduled(fixedDelay = 5000)
public void doNotify() throws IOException {
System.out.println("------##### inside doNotify");
System.out.println("##### ---- curent thread /notification : " + Thread.currentThread().getName());
if (SecurityContextHolder.getContext().getAuthentication() != null) {
Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
if (principal instanceof UserDetails) {
String username = ((UserDetails) principal).getUsername();
System.out.println("------##### principal instanceof UserDetails : " + username);
} else {
String username = principal.toString();
System.out.println("------##### principal : " + username);
}
}
}
}
the controller :
#Controller
#CrossOrigin(origins = "*")
public class SsePushNotificationRestController {
#Autowired
SsePushNotificationService service;
#Autowired
UserDetailsServiceImpl userService;
#Autowired
UserNotificationService userNotifService;
final List<SseEmitter> emitters = new CopyOnWriteArrayList<>();
String username;
int nbrEvent;
#GetMapping(value = "/notification", produces = { MediaType.TEXT_EVENT_STREAM_VALUE })
public ResponseEntity<SseEmitter> doNotify() throws InterruptedException, IOException {
System.out.println("##### ---- curent thread /notification : " + Thread.currentThread().getName());
final SseEmitter emitter = new SseEmitter();
service.addEmitter(emitter);
service.doNotify();
emitter.onCompletion(() -> service.removeEmitter(emitter));
emitter.onTimeout(() -> service.removeEmitter(emitter));
return new ResponseEntity<>(emitter, HttpStatus.OK);
}
}
Javascript :
const eventSource = new EventSource('http://localhost:8080/notification');
eventSource.onmessage = e => {
const msg = e.data;
$("#notifCounter").text(msg);
$("#usrNotifCounter").text(msg);
};
eventSource.onopen = e => console.log('open');
eventSource.onerror = e => {
if (e.readyState == EventSource.CLOSED) {
console.log('close');
}
else {
console.log(e);
}
};
eventSource.addEventListener('second', function(e) {
console.log('second', e.data);
}, false);
WebSecurityConfig :
#Configuration
#EnableWebSecurity
public class WebSecurityConfig<S extends Session> extends WebSecurityConfigurerAdapter {
#Autowired
private FindByIndexNameSessionRepository<S> sessionRepository;
#Autowired
private MySessionExpiredStrategy sessionExpiredStrategy;
#Bean
public UserDetailsService userDetailsService() {
return new UserDetailsServiceImpl();
}
#Bean
public BCryptPasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
#Bean
public DaoAuthenticationProvider authenticationProvider() {
DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider();
authProvider.setUserDetailsService(userDetailsService());
authProvider.setPasswordEncoder(passwordEncoder());
return authProvider;
}
#Component
public class MySessionExpiredStrategy implements SessionInformationExpiredStrategy {
#Override
public void onExpiredSessionDetected(SessionInformationExpiredEvent event)
throws IOException, ServletException {
HttpServletResponse response = event.getResponse();
response.setStatus(HttpStatus.UNAUTHORIZED.value());
response.setContentType("application/json;charset=utf-8");
response.getWriter().write(
"Your account has been logged in elsewhere, and the current login has expired. If the password is leaked, please change it immediately!");
}
}
#Bean
public SpringSessionBackedSessionRegistry<S> sessionRegistry() {
return new SpringSessionBackedSessionRegistry<>(this.sessionRepository);
}
#Bean
public ConcurrentSessionControlAuthenticationStrategy sessionControlAuthenticationStrategy() {
ConcurrentSessionControlAuthenticationStrategy csas = new ConcurrentSessionControlAuthenticationStrategy(
sessionRegistry());
csas.setExceptionIfMaximumExceeded(true);
return csas;
}
#Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers("/resources/**", "/static/**", "/css/**", "/js/**", "/img/**", "/error");
}
#Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.authenticationProvider(authenticationProvider());
}
#Override
protected void configure(HttpSecurity http) throws Exception {
.anyRequest().access("#rbacService.hasPermission(request,authentication)")
.and().formLogin().loginPage("/login").defaultSuccessUrl("/", true).permitAll().and().logout()
.deleteCookies("JSESSIONID").invalidateHttpSession(true).clearAuthentication(true)
.logoutRequestMatcher(new AntPathRequestMatcher("/logout")).logoutSuccessUrl("/login?logout")
.permitAll().and().exceptionHandling().accessDeniedPage("/static/403")
.and().sessionManagement().sessionFixation().migrateSession()
.sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)
.invalidSessionUrl("/static/invalidSession.html").maximumSessions(2).maxSessionsPreventsLogin(false)
.expiredSessionStrategy(sessionExpiredStrategy).sessionRegistry(sessionRegistry())
.expiredUrl("/login?invalid-session=true");
}
}
what is the best approch to share SecurityContextHolder between threads in that case.

Use sAMAccountName instead of userPrincipalName in LDAP auth in Spring Boot

So I am using the code below to connect to our LDAP server in my spring boot app, I can authorized successfully using the userPrincipalName -> (LastName.FirstName#enterprise.com), but I want to use the assigned sAMAccountName instead (ID00001).
I messed around with the setSearchFilter by doing: provider.setSearchFilter("(sAMAccountName ={0})")
but it's not working. (I'm getting bad credentials)
#EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.antMatcher("/myapplication")
.authorizeRequests()
.anyRequest().fullyAuthenticated()
.and()
.httpBasic();
}
#Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.authenticationProvider(activeDirectoryLdapAuthenticationProvider());
}
#Bean
public AuthenticationManager authenticationManager() {
return new ProviderManager(Arrays.asList(activeDirectoryLdapAuthenticationProvider()));
}
#Bean
public AuthenticationProvider activeDirectoryLdapAuthenticationProvider() {
ActiveDirectoryLdapAuthenticationProvider provider = new ActiveDirectoryLdapAuthenticationProvider("", "ldap://test.enterprise.com","dc=ORG1, dc=ORG2");
provider.setConvertSubErrorCodesToExceptions(true);
provider.setUseAuthenticationRequestCredentials(true);
provider.setSearchFilter("(sAMAccountName={0})"); // THIS DOES NOT WORK
provider.setSearchFilter("(userPrincipalName={0})"); // THIS WORKS
return provider;
}
}
EDIT: OK turns out I have been using the wrong field, there is another field: sAMAccountName that has the same value that I should be using, updated the title and question contents.
EDIT:
Ok I tried:
provider.setSearchFilter("(&(objectClass=user)(cn={0}))");
provider.setSearchFilter("(&(objectClass=user)(sAMAccountName={0}))");
provider.setSearchFilter("(&(objectClass=user)(cn={1}))");
provider.setSearchFilter("(&(objectClass=user)(sAMAccountName={1}))");
and it's still the same error,
bad credentials, AcceptSecurityContextError, 52e v2580
I managed to solve my issue but instead of using spring ldap dependencies, I created a custom authentication provider that implements AuthenticationProvider and and used the following code to connect to ldap and validate the credentials:
autowire it to WebSecurityConfig class.
#Component
public class CustomAuthenticationProvider implements AuthenticationProvider {
#Autowired
private UserDetails userDetails;
#Override
public Authentication authenticate(Authentication auth) throws
AuthenticationException {
String user = authentication.getName();
String pass = authentication.getCredentials.ToString();
try {
if (isRegistrered(user,pass) {
return new UsernamePasswordAuthenticationToken(user,pass, new
ArrayList<>());
} else {
return null;
}
} catch (Exception e) {
e.printStackTrace();
}
}
#Override
public boolean supports(Class<?> authentication) {
return authentication.equals(UsernamePasswordAuthenticationToken.class);
}
private isRegisterd(String user, String pass) {
boolean result = false;
try {
// Set up the environment for creating the initial context
Hashtable<String, String> env = new Hashtable<String, String>();
env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory");
env.put(Context.PROVIDER_URL, "ldap://ldap_server:389");
//
env.put(Context.SECURITY_AUTHENTICATION, "simple");
env.put(Context.SECURITY_PRINCIPAL, "DOMAIN\\USER");
env.put(Context.SECURITY_CREDENTIALS, "PASSWORD");
// Create the initial context
DirContext ctx = new InitialDirContext(env);
if(ctx != null) {
ctx.close();
}
return result;
} catch (Exception e) {
return result;
}
}
private void retrieveUserDetails(DirContext ctx, String username) throws NamingException {
String userSearchBase = "dc=TEST,dc=SAMPLE";
String userSearchFilter = String.format("sAMAccountName=%s", username);
//any attributes that you want
String[] attributes = { "sAMAccountName", "department" };
SearchControls controls = new SearchControls();
controls.setSearchScope(SearchControls.SUBTREE_SCOPE);
controls.setReturningAttributes(attributes);
SearchResult result = null;
NamingEnumeration<?> users = ctx.search(userSearchBase, userSearchFilter, controls);
while(users.hasmore()) {
result = (SearchResult) users.next();
Attributes attr = result.getAttribtutes();
String sAMAccountName = attr.get("sAMAccountName").get(0).toString();
String department = attr.get("department").get(0).toString();
//assign to autowired object to be accessed anywhere
this.userDetails.setAccountName(sAMAccountName);
this.userDetails.setDepartment(department);
}
}
}
There's another approach by using LdapAuthenticationProvider. I use LdapAuthenticationProvider with a single parameter constructor.
#Configuration
#EnableWebSecurity
#EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
#Profile("integration")
public class SecurityConfigurationT extends WebSecurityConfigurerAdapter {
private final AuthenticationManagerBuilder authenticationManagerBuilder;
private final TokenProvider tokenProvider;
public SecurityConfigurationT(
AuthenticationManagerBuilder authenticationManagerBuilder,
TokenProvider tokenProvider) {
this.authenticationManagerBuilder = authenticationManagerBuilder;
this.tokenProvider = tokenProvider;
}
#PostConstruct
public void initIntegration() {
try {
authenticationManagerBuilder
.authenticationProvider(ldapAuthenticationProvider());
} catch (Exception e) {
throw new BeanInitializationException("Security configuration failed", e);
}
}
#Override
#Bean
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
#Bean
public LdapAuthenticationProvider ldapAuthenticationProvider() throws Exception {
return new LdapAuthenticationProvider(ldapAuthenticator());
}
#Bean
public LdapContextSource ldapContextSource() throws Exception {
PasswordPolicyAwareContextSource contextSource = new PasswordPolicyAwareContextSource("ldaps://int.root.company.ag:636");
contextSource.setUserDn("CN=system_user,OU=companygroup svc accs,DC=int,DC=root,DC=company,DC=ag");
contextSource.setPassword("XXXXXX");
return contextSource;
}
// Use this for other filter such as "sAMAccountName".
#Bean
public LdapAuthenticator ldapAuthenticator() {
BindAuthenticator authenticator = new BindAuthenticator(ldapContextSource());
authenticator.setUserSearch(new FilterBasedLdapUserSearch("OU=company,OU=companygroup users,DC=int,DC=root,DC=company,DC=ag", "(sAMAccountName={0})", ldapContextSource()));
return authenticator;
}
}
Reference:
https://www.stevenschwenke.de/LDAPWithSpringSecurity

spring boot keycloak integration test

I am using keycloak to authenticate my spring boot application as below:
#Configuration
public class CustomKeycloakSpringBootConfigResolver extends KeycloakSpringBootConfigResolver {
private final KeycloakDeployment keycloakDeployment;
CustomKeycloakSpringBootConfigResolver(KeycloakSpringBootProperties properties) {
keycloakDeployment = KeycloakDeploymentBuilder.build(properties);
}
#Override
public KeycloakDeployment resolve(HttpFacade.Request facade) {
return keycloakDeployment;
}
#KeycloakConfiguration
class KeycloakSecurityConfig extends KeycloakWebSecurityConfigurerAdapter {
#Autowired
void configureGlobal(AuthenticationManagerBuilder auth) {
KeycloakAuthenticationProvider keycloakAuthenticationProvider = keycloakAuthenticationProvider();
keycloakAuthenticationProvider.setGrantedAuthoritiesMapper(new SimpleAuthorityMapper());
auth.authenticationProvider(keycloakAuthenticationProvider);
}
#Override
#Bean
protected SessionAuthenticationStrategy sessionAuthenticationStrategy() {
return new RegisterSessionAuthenticationStrategy(new SessionRegistryImpl());
}
#Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers("/resources/**");
}
#Override
protected void configure(HttpSecurity http) throws Exception {
super.configure(http);
http.authorizeRequests()
.antMatchers("/account/**").hasRole("user")
.anyRequest().permitAll().and()
.csrf().disable();
}
#Bean
#Override
#ConditionalOnMissingBean(HttpSessionManager.class)
protected HttpSessionManager httpSessionManager() {
return new HttpSessionManager();
}
}
i need to write integration test using mockmvc only, which will test that whenever a secured resource is accessed, the authentication is triggered to keycloak and after a successful authentication the resource is returned.
can anyone suggest how to achieve that.
As already mentioned in this answer, I wrote a lib to ease unit tests with a KeycloakAuthenticationToken in the SecurityContext.
You can browse a few sample apps with unit tests from here: https://github.com/ch4mpy/spring-addons/tree/master/samples.
Please note all samples run against a Keycloak server and that using keycloak spring-boot adapter libs might not be the best option:
not spring-boot 2.7+ complient (still extends WebSecurityConfigurerAdapter)
not WebFlux complient
very Keycloak adherent (you can hardly switch to another OIDC authorization server like Auth0, Microsoft Identity server, etc.)
KeycloakMessageServiceTest:
#ExtendWith(SpringExtension.class)
#Import(MessageServiceTest.TestConfig.class)
class MessageServiceTest {
#Autowired
MessageService service;
#WithMockKeycloakAuth(authorities = "USER", claims = #OpenIdClaims(preferredUsername = "ch4mpy"))
void whenAuthenticatedWithoutAuthorizedPersonnelThenCanNotGetSecret() {
assertThrows(AccessDeniedException.class, () -> service.getSecret());
}
#Test()
#WithMockKeycloakAuth(authorities = "AUTHORIZED_PERSONNEL", claims = #OpenIdClaims(preferredUsername = "ch4mpy"))
void whenAuthenticatedWitAuthorizedPersonnelThenGetSecret() {
final String actual = service.getSecret();
assertEquals("Secret message", actual);
}
#Test
void whenNotAuthenticatedThenCanNotGetGreeting() {
assertThrows(Exception.class, () -> service.greet(null));
}
#Test()
#WithMockKeycloakAuth(authorities = "AUTHORIZED_PERSONNEL", claims = #OpenIdClaims(preferredUsername = "ch4mpy"))
void whenAuthenticatedThenGetGreeting() {
final String actual = service.greet((KeycloakAuthenticationToken) SecurityContextHolder.getContext().getAuthentication());
assertEquals("Hello ch4mpy! You are granted with [AUTHORIZED_PERSONNEL].", actual);
}
#TestConfiguration(proxyBeanMethods = false)
#EnableGlobalMethodSecurity(prePostEnabled = true)
#Import({ MessageService.class })
static class TestConfig {
#Bean
GrantedAuthoritiesMapper authoritiesMapper() {
return new NullAuthoritiesMapper();
}
}
}
Controllers tests look like that:
#WebMvcTest(controllers = GreetingController.class)
class GreetingControllerAnnotatedTest {
private static final String GREETING = "Hello %s! You are granted with %s.";
#MockBean
MessageService messageService;
#MockBean
JwtDecoder jwtDecoder;
#Autowired
MockMvc api;
#BeforeEach
void setUp() {
when(messageService.greet(any())).thenAnswer(invocation -> {
final var auth = invocation.getArgument(0, Authentication.class);
return String.format(GREETING, auth.getName(), auth.getAuthorities());
});
}
// #formatter:off
#Test
#WithMockKeycloakAuth(
authorities = {"USER", "AUTHORIZED_PERSONNEL" },
claims = #OpenIdClaims(
sub = "42",
jti = "123-456-789",
nbf = "2020-11-18T20:38:00Z",
sessionState = "987-654-321",
email = "ch4mp#c4-soft.com",
emailVerified = true,
nickName = "Tonton-Pirate",
preferredUsername = "ch4mpy",
otherClaims = #Claims(jsonObjectClaims = #JsonObjectClaim(name = "foo", value = OTHER_CLAIMS))),
accessToken = #KeycloakAccessToken(
realmAccess = #KeycloakAccess(roles = { "TESTER" }),
authorization = #KeycloakAuthorization(permissions = #KeycloakPermission(rsid = "toto", rsname = "truc", scopes = "abracadabra")),
resourceAccess = {
#KeycloakResourceAccess(resourceId = "resourceA", access = #KeycloakAccess(roles = {"A_TESTER"})),
#KeycloakResourceAccess(resourceId = "resourceB", access = #KeycloakAccess(roles = {"B_TESTER"}))}))
// #formatter:on
void whenAuthenticatedWithKeycloakAuthenticationTokenThenCanGreet() throws Exception {
api
.perform(get("/greet"))
.andExpect(status().isOk())
.andExpect(content().string(startsWith("Hello ch4mpy! You are granted with ")))
.andExpect(content().string(containsString("AUTHORIZED_PERSONNEL")))
.andExpect(content().string(containsString("USER")))
.andExpect(content().string(containsString("TESTER")))
.andExpect(content().string(containsString("A_TESTER")))
.andExpect(content().string(containsString("B_TESTER")));
}
#Test
#WithMockKeycloakAuth
void testAuthentication() throws Exception {
api.perform(get("/authentication")).andExpect(status().isOk()).andExpect(content().string("Hello user"));
}
#Test
#WithMockKeycloakAuth
void testPrincipal() throws Exception {
api.perform(get("/principal")).andExpect(status().isOk()).andExpect(content().string("Hello user"));
}
static final String OTHER_CLAIMS = "{\"bar\":\"bad\", \"nested\":{\"deep\":\"her\"}, \"arr\":[1,2,3]}";
}

Adding custom header to response in spring rest / spring boot

i am trying to send session id in response header in rest controller but excludepathpattern() seems not working
** the configuration class is not triggering **
i have tried changing the sevlet version but it didnt work
ContextListener
#Override
public void contextInitialized(ServletContextEvent sce) {
ServletContext context = sce.getServletContext();
Map<String, HttpSession> map = new HashMap<>();
context.setAttribute("activeUsers", map);
HttpSessionListener
ServletContext context = session.getServletContext();
Map<String, HttpSession> activeUsers = (Map<String, HttpSession>) context.getAttribute("activeUsers");
activeUsers.put(session.getId(), session);
HandlerInterceptor
ServletContext context = request.getServletContext();
Map<String, HttpSession> activeUsers = (Map<String, HttpSession>) context.getAttribute("activeUsers");
String sessionId = request.getHeader("sessionId");
String requestUrl = request.getRequestURL().toString();
if (requestUrl.contains("/getOtp") || requestUrl.contains("/validateOtp")) {
return true;
} else {
if (activeUsers.containsKey(sessionId)) {
return true;
} else {
response.setStatus(401);
return false;
}
}
interceptorconfigurartion by extendig websecurityconfigure
#Configuration
#EnableAutoConfiguration
public class SessionInterceptorConfig implements WebMvcConfigurer {
#Autowired
private SessionHanlderInterceptor sessionHandlerIntercepto;
#Override
public void addInterceptors(InterceptorRegistry registry) {
// List<String> paths = new ArrayList<String>();
// paths.add("/auth/*");
registry.addInterceptor(sessionHandlerIntercepto).excludePathPatterns("/auth/**");
}
#Bean
public ServletListenerRegistrationBean<CustomSessionListener> filterRegistrationBean() {
ServletListenerRegistrationBean<CustomSessionListener> registrationBean = new ServletListenerRegistrationBean<CustomSessionListener>();
CustomSessionListener customURLFilter = new CustomSessionListener();
registrationBean.setListener(customURLFilter);
registrationBean.setOrder(1); // set precedence
return registrationBean;
}
#Bean
public ServletListenerRegistrationBean<CustomServletContextListener> filterContextRregistration() {
ServletListenerRegistrationBean<CustomServletContextListener> registrationBean = new ServletListenerRegistrationBean<CustomServletContextListener>();
CustomServletContextListener customURLFilter = new CustomServletContextListener();
registrationBean.setListener(customURLFilter);
registrationBean.setOrder(1); // set precedence
return registrationBean;
}
Sprinboot main class
#SpringBootApplication
public class CustomerApplication extends SpringBootServletInitializer {
i expect to add the session id to header in response and to check for the sessionid in request
You can use spring web component "OncePerRequestFilter". You need to inject a bean which extends OncePerRequestFilter. Example:
public class CustomHeaderFilter extends OncePerRequestFilter {
#Override
public void doFilterInternal(final HttpServletRequest request, final HttpServletResponse response,
final FilterChain chain) throws IOException, ServletException {
response.setHeader(customHeaderName, customHeaderValue);
chain.doFilter(request, response);
}
}

Expose Togglz Admin console in Spring Boot on management-port

By default Togglz admin console runs on application port (configured by server.port property). I want to expose it on management.port. My question: is it possible?
If you use Togglz >= 2.4.0 then this feature is available out of the box.
For older releases solution is below:
I managed to expose a raw servlet on management.port by wrapping it with MvcEndpoint.
The easiest way to do it to use Spring Cloud module which does all the job for you (for example in the HystrixStreamEndpoint):
public class HystrixStreamEndpoint extends ServletWrappingEndpoint {
public HystrixStreamEndpoint() {
super(HystrixMetricsStreamServlet.class, "hystrixStream", "/hystrix.stream",
true, true);
}
}
In the case of TogglzConsoleServlet there is unfortunately one more hack to do with path's due to the way it extracts prefix from request URI, so the whole solution looks a little bit ugly:
#Component
class TogglzConsoleEndpoint implements MvcEndpoint {
private static final String ADMIN_CONSOLE_URL = "/togglz-console";
private final TogglzConsoleServlet togglzConsoleServlet;
#Autowired
TogglzConsoleEndpoint(final ServletContext servletContext) throws ServletException {
this.togglzConsoleServlet = new TogglzConsoleServlet();
togglzConsoleServlet.init(new DelegatingServletConfig(servletContext));
}
#Override
public String getPath() {
return ADMIN_CONSOLE_URL;
}
#Override
public boolean isSensitive() {
return true;
}
#Override
public Class<? extends Endpoint> getEndpointType() {
return null;
}
#RequestMapping("**")
public ModelAndView handle(HttpServletRequest request, HttpServletResponse response) throws Exception {
HttpServletRequestWrapper requestWrapper = new HttpServletRequestWrapper(request) {
#Override
public String getServletPath() {
return ADMIN_CONSOLE_URL;
}
};
togglzConsoleServlet.service(requestWrapper, response);
return null;
}
private class DelegatingServletConfig implements ServletConfig {
private final ServletContext servletContext;
DelegatingServletConfig(final ServletContext servletContext) {
this.servletContext = servletContext;
}
#Override
public String getServletName() {
return TogglzConsoleEndpoint.this.togglzConsoleServlet.getServletName();
}
#Override
public ServletContext getServletContext() {
return servletContext;
}
#Override
public String getInitParameter(final String name) {
return servletContext.getInitParameter(name);
}
#Override
public Enumeration<String> getInitParameterNames() {
return servletContext.getInitParameterNames();
}
}
}

Resources