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

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.

Related

I can't update my webapp to Spring Boot 2.6.0 (2.5.7 works but 2.6.0 doesn't)

As mentioned in the title I can't update my webapp to Spring Boot 2.6.0. I wrote my webapp using Spring Boot 2.5.5 and everything works perfectly. If I update the pom.xml file with this new tag:
<version>2.5.7</version>
My webapp works perfectly. All tests work.
If I perform this update the webapp does not start:
<version>2.6.0</version>
Starting the DEBUG mode the IDE shows me an error and 2 links to 2 classes of my webapp.
2021-11-23 00:31:45.419 ERROR 21884 --- [ restartedMain] o.s.boot.SpringApplication : Application run failed
org.springframework.beans.factory.BeanCurrentlyInCreationException: Error creating bean with name 'configurazioneSpringSecurity': Requested bean is currently in creation: Is there an unresolvable circular reference?
It seems the problem is in this class:
#Configuration
#EnableWebSecurity
public class ConfigurazioneSpringSecurity extends WebSecurityConfigurerAdapter {
#Autowired
LivelliDeiRuoli livelliDeiRuoli;
#Autowired
GestioneUtentiSpringSecurity gestioneUtentiSpringSecurity;
#Bean
public BCryptPasswordEncoder metodoCrittografia() {
return new BCryptPasswordEncoder();
}
#Autowired
public void crittografiaPassword(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(gestioneUtentiSpringSecurity).passwordEncoder(metodoCrittografia());
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable();
http.authorizeRequests().antMatchers(
"/",
"/login",
"/benvenuto",
"/registrazione",
"/registrazione-eseguita",
"/pagine-applicazione"
).permitAll();
http.authorizeRequests().antMatchers("/area-riservata")
.access("hasAnyRole('" + livelliDeiRuoli.elencoRuoli(1L) + "')");
http.authorizeRequests().antMatchers("/cambio-password")
.access("hasAnyRole('" + livelliDeiRuoli.elencoRuoli(1L) + "')");
http.authorizeRequests().antMatchers("/cambio-nome")
.access("hasAnyRole('" + livelliDeiRuoli.elencoRuoli(1L) + "')");
http.authorizeRequests().antMatchers("/cancella-utente")
.access("isAuthenticated()");
http.authorizeRequests().antMatchers("/gestione-utenti")
.access("hasAnyRole('" + livelliDeiRuoli.elencoRuoli(2L) + "')");
http.authorizeRequests().antMatchers("/gestione-ruoli")
.access("hasAnyRole('" + livelliDeiRuoli.elencoRuoli(3L) + "')");
http.authorizeRequests().antMatchers("/pannello-di-controllo")
.access("hasAnyRole('" + livelliDeiRuoli.elencoRuoli(3L) + "')");
http.authorizeRequests().and().exceptionHandling().accessDeniedPage("/errore-403");
http.authorizeRequests().and().formLogin()
.loginProcessingUrl("/pagina-login")
.loginPage("/login")
.defaultSuccessUrl("/")
.failureUrl("/login?errore=true")
.usernameParameter("username")
.passwordParameter("password")
.and().logout().logoutUrl("/pagina-logout")
.logoutSuccessUrl("/login?logout=true");
http.authorizeRequests().and() //
.rememberMe().tokenRepository(this.persistentTokenRepository()) //
.tokenValiditySeconds(365 * 24 * 60 * 60);
http.authorizeRequests().antMatchers("/gestione-eventi")
.access("hasAnyRole('" + livelliDeiRuoli.elencoRuoli(2L) + "')");
http.authorizeRequests().antMatchers(
"/cerca-eventi",
"/ultimi-eventi"
).permitAll();
}
#Autowired
private DataSource dataSource;
#Bean
public PersistentTokenRepository persistentTokenRepository() {
JdbcTokenRepositoryImpl db = new JdbcTokenRepositoryImpl();
db.setDataSource(dataSource);
return db;
}
#Bean(name = BeanIds.AUTHENTICATION_MANAGER)
#Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
}
or in this:
#SpringBootApplication
#Profile("sviluppo")
public class GestioneUtentiApplication extends SpringBootServletInitializer {
#Override
protected SpringApplicationBuilder configure(SpringApplicationBuilder application) {
return application.sources(GestioneUtentiApplication.class);
}
public static void main(String[] args) {
System.setProperty("server.servlet.context-path", "/gestioneutenti");
SpringApplication.run(GestioneUtentiApplication.class, args);
}
}
What's wrong with these classes?
What changes with Spring Boot 2.6.0?
GestioneUtentiSpringSecurity implements UserDetailsService:
#Service
public class GestioneUtentiSpringSecurity implements UserDetailsService {
#Autowired
private UtenteRepository utenteRepository;
#Autowired
private RuoloRepository ruoloRepository;
#Autowired
EseguiVariabiliDiSistema eseguiVariabiliDiSistema;
#Autowired
LivelliDeiRuoli livelliDeiRuoli;
#Override
public UserDetails loadUserByUsername(String nomeUtente) throws UsernameNotFoundException {
Utente utente = trovaUtenteConPrivilegiDiAutenticazione(nomeUtente);
if (utente == null) {
throw new UsernameNotFoundException("L'utente " + nomeUtente + " non รจ stato trovato nel database.");
}
List<String> ruoliUtente = null;
try {
ruoliUtente = this.ruoloRepository.trovaRuoliUtente(utente.getId());
}catch (Exception b){
ruoliUtente = null;
}
List<GrantedAuthority> grantList = null;
try{
grantList = new ArrayList<GrantedAuthority>();
if (ruoliUtente != null) {
for (String ruolo : ruoliUtente) {
GrantedAuthority authority = new SimpleGrantedAuthority(ruolo);
grantList.add(authority);
}
}
}catch (Exception c){
grantList = null;
}
UserDetails userDetails = null;
if((utente != null) && (ruoliUtente != null) && (grantList != null)){
userDetails = (UserDetails) new User(utente.getNome(), utente.getPassword(), grantList);
}
return userDetails;
}
public Utente trovaUtenteConPrivilegiDiAutenticazione(String nomeUtente){
try{
Utente utente = utenteRepository.trovaUtente(nomeUtente);
if(livelliDeiRuoli.requisitiUtenteConRuoloMassimo(utente)){
return utente;
} else{
eseguiVariabiliDiSistema.trovaVariabileSenzaVerificaUtente(
new VariabileSistema(0L, "login", "")
);
if(eseguiVariabiliDiSistema.getVariabileDiSistema().getValore().equals("true")){
return utente;
}else if(eseguiVariabiliDiSistema.getVariabileDiSistema().getValore().equals("false")){
return null;
}else{
return null;
}
}
}catch (Exception e){
return null;
}
}
}
Starting on Spring Boot 2.6, circular dependencies are prohibited by default. you can allow circular references again by setting the following property:
spring.main.allow-circular-references = true
You can read some more details about this in the Spring Boot 2.6 Release Notes.
The problem that Spring faces here and causes to not able to move forward starting from spring boot 2.6 with the default configuration of spring.main.allow-circular-references = false is located in the following part
#Bean
public BCryptPasswordEncoder metodoCrittografia() {
return new BCryptPasswordEncoder();
}
#Autowired
public void crittografiaPassword(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(gestioneUtentiSpringSecurity).passwordEncoder(metodoCrittografia());
}
I believe this is happening because the WebSecurityConfig extends WebSecurityConfigurerAdapter has some circular references in combination with BCryptPasswordEncoder inside this class.
The solution is to create another configuration class, where you can split the configurations so that spring is able to correctly create the beans avoiding circular references.
So you can create the following extra class
#Configuration
public class CustomSecurityConfig {
#Bean
public BCryptPasswordEncoder metodoCrittografia() {
return new BCryptPasswordEncoder();
}
}
Then in your original ConfigurazioneSpringSecurity.class you can replace the failing
#Bean
public BCryptPasswordEncoder metodoCrittografia() {
return new BCryptPasswordEncoder();
}
#Autowired
public void crittografiaPassword(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(gestioneUtentiSpringSecurity).passwordEncoder(metodoCrittografia());
}
with the
#Autowired
private PasswordEncoder passwordEncoder;
#Autowired
public void crittografiaPassword(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(gestioneUtentiSpringSecurity)
.passwordEncoder(passwordEncoder);
}
Although setting the application.properties works, it is likely using a feature that is going to be deprecated. I was able to work around this by using setter based injection. It's a bit more verbose but might be a good starting point for those looking to stay current and not use features that might be deprecated down the line.
It's certainly an answer that can be improved upon and I hope others can contribute perhaps more concise answers. I'll update this if I find anything cleaner.
Before
#Component
public class CustomFilter extends OncePerRequestFilter {
#Autowired
private MyUserDetailsService myUserDetailsService;
#Autowired
private JWTUtils jwtUtils;
//When any api will be called this method will be called first and this will extract
// Token from header pass to JWT Util calls for token details extraction
#Override
protected void doFilterInternal(HttpServletRequest httpServletRequest,
HttpServletResponse httpServletResponse, FilterChain filterChain)
throws ServletException, IOException {
//implementation
}
}
After
#Component
public class CustomFilter extends OncePerRequestFilter {
private MyUserDetailsService myUserDetailsService;
public void setMyUserDetailsService(MyUserDetailsService myUserDetailsService) {
this.myUserDetailsService = myUserDetailsService;
}
public void setJwtUtils(JWTUtils jwtUtils) {
this.jwtUtils = jwtUtils;
}
private JWTUtils jwtUtils;
//When any api will be called this method will be called first and this will extract
// Token from header pass to JWT Util calls for token details extraction
#Override
protected void doFilterInternal(HttpServletRequest httpServletRequest,
HttpServletResponse httpServletResponse, FilterChain filterChain)
throws ServletException, IOException {
//implementation
}
}
reference:
https://theintuitiveprogrammer.com/post-eight.html
I've this problem during migrate to spring boot 2.6.x with WebSecurityConfig code:
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
#Bean
public UserDetailsService userDetailsService() {
return email -> {
....
};
}
#Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService())
...;
}
fix according WebSecurityConfigurerAdapter#userDetailsServiceBean javadoc:
Override this method to expose a UserDetailsService created from configure(AuthenticationManagerBuilder) as a bean ...
To change the instance returned, developers should change userDetailsService() instead
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
#Bean
#Override
public UserDetailsService userDetailsServiceBean() throws Exception {
return super.userDetailsServiceBean();
}
#Override
public UserDetailsService userDetailsService() {
return email -> {
....
};
}
#Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService())
...;
}

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]}";
}

Spring Boot Social Login and Local OAuth2-Server

I'm currently working on a Spring Boot-Application with OAuth2-Authentication. I have a local OAuth2-Server where I receive a token when posting username and password of the local database against in my case http://localhost:8080/v1/oauth/token using Spring Boot's UserDetails and UserService. Everything works fine and nice.
But now I want to enhance my program with Facebook social login and want either log in to my local OAuth2-Server or using the external Facebook-Server. I checked out the Spring Boot example https://spring.io/guides/tutorials/spring-boot-oauth2/ and adapted the idea of an SSO-Filter. Now I can login using my Facebook client and secret id, but I cannot access my restricted localhost-sites.
What I want is that the Facebook-Token "behaves" the same way as the locally generated tokens by for instance being part of my local token storage. I checked out several tutorials and other Stackoverflow questions but with no luck. Here is what I have so far with a custom Authorization-Server and I think I'm still missing something very basic to get the link between external Facebook- and internal localhost-Server:
#Configuration
public class OAuth2ServerConfiguration {
private static final String SERVER_RESOURCE_ID = "oauth2-server";
#Autowired
private TokenStore tokenStore;
#Bean
public TokenStore tokenStore() {
return new InMemoryTokenStore();
}
protected class ClientResources {
#NestedConfigurationProperty
private AuthorizationCodeResourceDetails client = new AuthorizationCodeResourceDetails();
#NestedConfigurationProperty
private ResourceServerProperties resource = new ResourceServerProperties();
public AuthorizationCodeResourceDetails getClient() {
return client;
}
public ResourceServerProperties getResource() {
return resource;
}
}
#Configuration
#EnableResourceServer
#EnableOAuth2Client
protected class ResourceServerConfiguration extends ResourceServerConfigurerAdapter {
#Value("${pia.requireauth}")
private boolean requireAuth;
#Override
public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
resources.tokenStore(tokenStore).resourceId(SERVER_RESOURCE_ID);
}
#Autowired
OAuth2ClientContext oauth2ClientContext;
#Bean
public FilterRegistrationBean oauth2ClientFilterRegistration(OAuth2ClientContextFilter filter) {
FilterRegistrationBean registration = new FilterRegistrationBean();
registration.setFilter(filter);
registration.setOrder(-100);
return registration;
}
#Bean
#ConfigurationProperties("facebook")
public ClientResources facebook() {
return new ClientResources();
}
private Filter ssoFilter() {
CompositeFilter filter = new CompositeFilter();
List<Filter> filters = new ArrayList<>();
filters.add(ssoFilter(facebook(), "/login/facebook"));
filter.setFilters(filters);
return filter;
}
private Filter ssoFilter(ClientResources client, String path) {
OAuth2ClientAuthenticationProcessingFilter filter = new OAuth2ClientAuthenticationProcessingFilter(path);
OAuth2RestTemplate template = new OAuth2RestTemplate(client.getClient(), oauth2ClientContext);
filter.setRestTemplate(template);
UserInfoTokenServices tokenServices = new UserInfoTokenServices(client.getResource().getUserInfoUri(),
client.getClient().getClientId());
tokenServices.setRestTemplate(template);
filter.setTokenServices(tokenServices);
return filter;
}
#Override
public void configure(HttpSecurity http) throws Exception {
if (!requireAuth) {
http.antMatcher("/**").authorizeRequests().anyRequest().permitAll();
} else {
http.antMatcher("/**").authorizeRequests().antMatchers("/admin/**").hasRole("ADMIN")
.antMatchers("/", "/login**", "/webjars/**").permitAll().anyRequest().authenticated().and()
.exceptionHandling().and().csrf()
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()).and()
.addFilterBefore(ssoFilter(), BasicAuthenticationFilter.class);
}
}
}
#Configuration
#EnableAuthorizationServer
protected class OAuth2Configuration extends AuthorizationServerConfigurerAdapter {
#Value("${pia.oauth.tokenTimeout:3600}")
private int expiration;
#Autowired
private AuthenticationManager authenticationManager;
#Autowired
#Qualifier("userDetailsService")
private UserDetailsService userDetailsService;
// password encryptor
#Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
#Override
public void configure(AuthorizationServerEndpointsConfigurer configurer) throws Exception {
configurer.authenticationManager(authenticationManager).tokenStore(tokenStore).approvalStoreDisabled();
configurer.userDetailsService(userDetailsService);
}
#Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.inMemory().withClient("pia").secret("alphaport").accessTokenValiditySeconds(expiration)
.authorities("ROLE_USER").scopes("read", "write").authorizedGrantTypes("password", "refresh_token")
.resourceIds(SERVER_RESOURCE_ID);
}
}
}
Any help and/or examples covering this issue greatly appreciated! :)
One possible solution is to implement the Authentication Filter and Authentication Provider.
In my case I've implemented an OAuth2 authentication and also permit the user to access some endpoints with facebook access_token
The Authentication Filter looks like this:
public class ServerAuthenticationFilter extends GenericFilterBean {
private BearerAuthenticationProvider bearerAuthenticationProvider;
private FacebookAuthenticationProvider facebookAuthenticationProvider;
public ServerAuthenticationFilter(BearerAuthenticationProvider bearerAuthenticationProvider,
FacebookAuthenticationProvider facebookAuthenticationProvider) {
this.bearerAuthenticationProvider = bearerAuthenticationProvider;
this.facebookAuthenticationProvider = facebookAuthenticationProvider;
}
#Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest httpRequest = (HttpServletRequest) request;
HttpServletResponse httpResponse = (HttpServletResponse) response;
Optional<String> authorization = Optional.fromNullable(httpRequest.getHeader("Authorization"));
try {
AuthType authType = getAuthType(authorization.get());
if (authType == null) {
SecurityContextHolder.clearContext();
httpResponse.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
}
String strToken = authorization.get().split(" ")[1];
if (authType == AuthType.BEARER) {
if (strToken != null) {
Optional<String> token = Optional.of(strToken);
logger.debug("Trying to authenticate user by Bearer method. Token: " + token.get());
processBearerAuthentication(token);
}
} else if (authType == AuthType.FACEBOOK) {
if (strToken != null) {
Optional<String> token = Optional.of(strToken);
logger.debug("Trying to authenticate user by Facebook method. Token: " + token.get());
processFacebookAuthentication(token);
}
}
logger.debug(getClass().getSimpleName() + " is passing request down the filter chain.");
chain.doFilter(request, response);
} catch (InternalAuthenticationServiceException internalAuthenticationServiceException) {
SecurityContextHolder.clearContext();
logger.error("Internal Authentication Service Exception", internalAuthenticationServiceException);
httpResponse.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
} catch (AuthenticationException authenticationException) {
SecurityContextHolder.clearContext();
httpResponse.sendError(HttpServletResponse.SC_UNAUTHORIZED, authenticationException.getMessage());
} catch (Exception e) {
SecurityContextHolder.clearContext();
e.printStackTrace();
httpResponse.sendError(HttpServletResponse.SC_UNAUTHORIZED, e.getMessage());
}
}
private AuthType getAuthType(String value) {
if (value == null)
return null;
String[] basicSplit = value.split(" ");
if (basicSplit.length != 2)
return null;
if (basicSplit[0].equalsIgnoreCase("bearer"))
return AuthType.BEARER;
if (basicSplit[0].equalsIgnoreCase("facebook"))
return AuthType.FACEBOOK;
return null;
}
private void processBearerAuthentication(Optional<String> token) {
Authentication resultOfAuthentication = tryToAuthenticateWithBearer(token);
SecurityContextHolder.getContext().setAuthentication(resultOfAuthentication);
}
private void processFacebookAuthentication(Optional<String> token) {
Authentication resultOfAuthentication = tryToAuthenticateWithFacebook(token);
SecurityContextHolder.getContext().setAuthentication(resultOfAuthentication);
}
private Authentication tryToAuthenticateWithBearer(Optional<String> token) {
PreAuthenticatedAuthenticationToken requestAuthentication = new PreAuthenticatedAuthenticationToken(token,
null);
return tryToAuthenticateBearer(requestAuthentication);
}
private Authentication tryToAuthenticateWithFacebook(Optional<String> token) {
PreAuthenticatedAuthenticationToken requestAuthentication = new PreAuthenticatedAuthenticationToken(token,
null);
return tryToAuthenticateFacebook(requestAuthentication);
}
private Authentication tryToAuthenticateBearer(Authentication requestAuthentication) {
Authentication responseAuthentication = bearerAuthenticationProvider.authenticate(requestAuthentication);
if (responseAuthentication == null || !responseAuthentication.isAuthenticated()) {
throw new InternalAuthenticationServiceException(
"Unable to Authenticate for provided credentials.");
}
logger.debug("Application successfully authenticated by bearer method.");
return responseAuthentication;
}
private Authentication tryToAuthenticateFacebook(Authentication requestAuthentication) {
Authentication responseAuthentication = facebookAuthenticationProvider.authenticate(requestAuthentication);
if (responseAuthentication == null || !responseAuthentication.isAuthenticated()) {
throw new InternalAuthenticationServiceException(
"Unable to Authenticate for provided credentials.");
}
logger.debug("Application successfully authenticated by facebook method.");
return responseAuthentication;
}
}
This, filters Authorization headers, identifies whether they are facebook or bearer and then directs to specific provider.
The Facebook Provider looks like this:
public class FacebookAuthenticationProvider implements AuthenticationProvider {
#Value("${config.oauth2.facebook.resourceURL}")
private String facebookResourceURL;
private static final String PARAMETERS = "fields=name,email,gender,picture";
#Autowired
FacebookUserRepository facebookUserRepository;
#Autowired
UserRoleRepository userRoleRepository;
#SuppressWarnings({ "rawtypes", "unchecked" })
#Override
public Authentication authenticate(Authentication auth) throws AuthenticationException {
Optional<String> token = auth.getPrincipal() instanceof Optional ? (Optional) auth.getPrincipal() : null;
if (token == null || !token.isPresent() || token.get().isEmpty())
throw new BadCredentialsException("Invalid Grants");
SocialResourceUtils socialResourceUtils = new SocialResourceUtils(facebookResourceURL, PARAMETERS);
SocialUser socialUser = socialResourceUtils.getResourceByToken(token.get());
if (socialUser != null && socialUser.getId() != null) {
User user = findOriginal(socialUser.getId());
if (user == null)
throw new BadCredentialsException("Authentication failed.");
Credentials credentials = new Credentials();
credentials.setId(user.getId());
credentials.setUsername(user.getEmail());
credentials.setName(user.getName());
credentials.setRoles(parseRoles(user.translateRoles()));
credentials.setToken(token.get());
return new UsernamePasswordAuthenticationToken(credentials, credentials.getId(),
parseAuthorities(getUserRoles(user.getId())));
} else
throw new BadCredentialsException("Authentication failed.");
}
protected User findOriginal(String id) {
FacebookUser facebookUser = facebookUserRepository.findByFacebookId(facebookId);
return null == facebookUser ? null : userRepository.findById(facebookUser.getUserId()).get();
}
protected List<String> getUserRoles(String id) {
List<String> roles = new ArrayList<>();
userRoleRepository.findByUserId(id).forEach(applicationRole -> roles.add(applicationRole.getRole()));
return roles;
}
private List<Roles> parseRoles(List<String> strRoles) {
List<Roles> roles = new ArrayList<>();
for(String strRole : strRoles) {
roles.add(Roles.valueOf(strRole));
}
return roles;
}
private Collection<? extends GrantedAuthority> parseAuthorities(Collection<String> roles) {
if (roles == null || roles.size() == 0)
return Collections.emptyList();
return roles.stream().map(role -> (GrantedAuthority) () -> "ROLE_" + role).collect(Collectors.toList());
}
#Override
public boolean supports(Class<?> auth) {
return auth.equals(UsernamePasswordAuthenticationToken.class);
}
}
The FacebookUser only makes a reference to the Local User Id and the Facebook Id (this is the link between facebook and our application).
This SocialResourceUtils is used to get the facebook user information via facebook API (using the method getResourceByToken). The facebook resource url is setted on application.properties (config.oauth2.facebook.resourceURL). This method is basically:
public SocialUser getResourceByToken(String token) {
RestTemplate restTemplate = new RestTemplate();
String authorization = token;
JsonNode response = null;
try {
response = restTemplate.getForObject(accessUrl + authorization, JsonNode.class);
} catch (RestClientException e) {
throw new BadCredentialsException("Authentication failed.");
}
return buildSocialUser(response);
}
The Bearer Provider is your local Authentication, you can make your own, or use the springboot defaults, use other authentication methods, idk (I will not put my implementation here, thats by you).
And finally you need to make your Web Security Configurer:
#ConditionalOnProperty("security.basic.enabled")
#Configuration
#EnableWebSecurity
#EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter {
#Autowired
private BearerAuthenticationProvider bearerAuthenticationProvider;
#Autowired
private FacebookAuthenticationProvider facebookAuthenticationProvider;
#Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable();
http.addFilterBefore(new ServerAuthenticationFilter(bearerAuthenticationProvider,
facebookAuthenticationProvider), BasicAuthenticationFilter.class);
}
}
Notice that it has the annotation ConditionalOnProperty to enable/disable on properties security.basic.enabled. The #EnableGlobalMethodSecurity(prePostEnabled = true) enables the usage of the annotation #PreAuthorize which enables us to protect endpoints by roles for example (using #PreAuthorize("hasRole ('ADMIN')") over an endpoint, to allow acces only to admins)
This code needs many improvements, but I hope I have helped.

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

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;
}
}

Resources