I have a custom authentication CustomAuthenticationProvider class which does a authenticate the user by hitting LDAP remote server. I managed to create and configure the custom authentication provider but having trouble to call doAuthentication method which is defined in SecurityConfiguration.
#Component
public class CustomAuthenticationProvider implements AuthenticationProvider {
private final Logger log=LoggerFactory.getLogger(CustomAuthenticationProvider.class);
#Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
String username = authentication.getName();
String password = (String) authentication.getCredentials();
if (username == null) {
throw new BadCredentialsException("User is not found");
}
if (password == null) {
throw new BadCredentialsException("Password is not found");
}
try {
LdapContextSource ldapContextSource = new LdapContextSource();
ldapContextSource.setUrl("ldap://jnj.com:3268");
ldapContextSource.setBase("dc=jnj,dc=com");
ldapContextSource.setUserDn(username);
ldapContextSource.setPassword(password);
try {
// initialize the context
ldapContextSource.afterPropertiesSet();
} catch (Exception e) {
e.printStackTrace();
}
LdapTemplate ldapTemplate = new LdapTemplate(ldapContextSource);
ldapTemplate.afterPropertiesSet();
// ldapTemplate.setIgnorePartialResultException(true); // Active Directory doesn’t transparently handle referrals. This fixes that.
AndFilter filter = new AndFilter();
filter.and(new EqualsFilter("sAMAccountName", username));
try {
boolean authed = ldapTemplate.authenticate("", filter.toString(), password);
log.debug("Auuthenticated : "+authed);
} catch (org.springframework.ldap.AuthenticationException ee) {
//userDisplay.setText(“Invalid Username/Password”);
}
} catch (Exception e) {
e.printStackTrace();
}
Collection<? extends GrantedAuthority> authorities = Collections.singleton(new SimpleGrantedAuthority("ROLE_USER"));
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(username, password, authorities);
return authenticationToken;
// return new UsernamePasswordAuthenticationToken(username,password);
}
#Override
public boolean supports(Class<?> authentication) {
return authentication.equals(UsernamePasswordAuthenticationToken.class);
}
}
This method is called in SecurityConfiguration,I need to bind this method to mine CustomAuthenticationProvider class
#Inject
public void configureGlobal(AuthenticationManagerBuilder auth) {
try {
auth.authenticationProvider(authProvider)
.ldapAuthentication().userDetailsContextMapper(userDetailsContextMapper())
.ldapAuthoritiesPopulator(ldapAuthoritiesPopulator()).userSearchBase(USER_SEARCH_BASE)
.userSearchFilter(USER_SEARCH_FILTER).groupSearchBase(GROUP_SEARCH_BASE)
.groupSearchFilter(GROUP_SEARCH_FILTER).contextSource(contextSource());
} catch (Exception e) {
throw new BeanInitializationException("Security configuration failed", e);
}
}
This method I need to call for LDAP
protected DirContextOperations doAuthentication(UsernamePasswordAuthenticationToken auth) {
String username = auth.getName();
String password = (String) auth.getCredentials();
DirContext ctx = bindAsUser(username, password);
try {
return searchForUser(ctx, username);
} catch (NamingException e) {
log.error("Failed to locate directory entry for authenticated user: " + username, e);
throw badCredentials(e);
} finally {
LdapUtils.closeContext(ctx);
}
}
Related
I´m trying to make a Unit test of the methods of my project which contains spring security.
When I run the project it works normally, but when I try to unit test the methods it gives me this error.
Description:
Pramenter 0 of constructor in ...config.secutityConfig required a bean of type 'org.springframeword.security.userdetails.UserDetailsService' that could not be found
Action:
Consider defining a bean of type 'org.springframework.security.core.userdetails.UserDetailsService in your configuration'
This is my SecurityConfig.java code:
#Configuration
#EnableWebSecurity
#RequiredArgsConstructor
public class SecurityConfig extends WebSecurityConfigurerAdapter {
#Autowired
private final UserDetailsService userDetailsService;
private final BCryptPasswordEncoder bCryptPasswordEncoder;
#Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService).passwordEncoder(bCryptPasswordEncoder);
}
#Override
protected void configure(HttpSecurity http) throws Exception {
CustomAuthenticationFilter customAuthenticationFilter = new CustomAuthenticationFilter(authenticationManagerBean());
http.csrf().disable();
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
http.authorizeRequests().antMatchers(GET, "/user/**").hasAnyAuthority("ADMIN");
http.authorizeRequests().antMatchers(POST, "/user/**").hasAnyAuthority("ADMIN");
http.authorizeRequests().antMatchers(PUT, "/user/**").hasAnyAuthority("ADMIN");
http.authorizeRequests().antMatchers(DELETE, "/user/**").hasAnyAuthority("ADMIN");
http.authorizeRequests().antMatchers(GET, "/category/**").hasAnyAuthority("ADMIN");
http.authorizeRequests().antMatchers(POST, "/category/**").hasAnyAuthority("ADMIN");
http.authorizeRequests().antMatchers(PUT, "/category/**").hasAnyAuthority("ADMIN");
http.authorizeRequests().antMatchers(DELETE, "/category/**").hasAnyAuthority("ADMIN");
http.authorizeRequests().antMatchers(GET, "/product/**").hasAnyAuthority("ADMIN");
http.authorizeRequests().antMatchers(POST, "/product/**").hasAnyAuthority("ADMIN");
http.authorizeRequests().antMatchers(PUT, "/product/**").hasAnyAuthority("ADMIN");
http.authorizeRequests().antMatchers(DELETE, "/product/**").hasAnyAuthority("ADMIN");
http.authorizeRequests().antMatchers(GET, "/shoppingcart/**").hasAnyAuthority("ADMIN");
http.authorizeRequests().antMatchers(POST, "/shoppingcart/**").hasAnyAuthority("ADMIN");
http.authorizeRequests().antMatchers(PUT, "/shoppingcart/**").hasAnyAuthority("ADMIN");
http.authorizeRequests().antMatchers(DELETE, "/shoppingcart/**").hasAnyAuthority("ADMIN");
http.authorizeRequests().antMatchers(GET, "/product/**").hasAnyAuthority("USER");
http.authorizeRequests().anyRequest().authenticated();
http.addFilter(customAuthenticationFilter);
http.addFilterBefore(new CustomAuthorizationFilter(), UsernamePasswordAuthenticationFilter.class);
}
#Bean
#Override
public AuthenticationManager authenticationManagerBean() throws Exception{
return super.authenticationManagerBean();
}
}
This is the test I'm trying to make, it's basically to save a category in the DB.
#WebMvcTest(controllers = CategoryRest.class)
public class CategoryRestTest extends AbstractUnitRestTest {
#MockBean
private CategoryService categoryService;
#Test
public void saveCategory() throws Exception {
CreateCategoryCmd cmd = new CreateCategoryCmd("Tehnika", "TV, USB", Collections.emptySet());
String jsonInString = mapper.writerWithDefaultPrettyPrinter().writeValueAsString(cmd);
Category category = CategoryBuilder.categoryBelaTehnika();
doReturn(category).when(categoryService).save(any(CreateCategoryCmd.class));
mockMvc.perform(post("/category/save")
.contentType(MediaType.APPLICATION_JSON)
.content(jsonInString)).andDo(print())
.andExpect(status().isOk())
.andExpect(jsonPath("$.name").value(category.getName()));
}
And this is my Userdetails:
#Service
#Transactional
#RequiredArgsConstructor
#Slf4j
public class UserServiceImpl implements UserService, UserDetailsService {
private final static Logger LOGGER = LoggerFactory.getLogger(UserServiceImpl.class);
private final UserDAO userDAO;
private final PayPalAccountDAO payPalAccountDAO;
private final RoleDao roleDao;
private final ShoppingCartDAO shoppingCartDAO;
private final PasswordEncoder passwordEncoder;
#Override
#Transactional
public User save(CreateUserCmd cmd) throws ServiceException {
User user = UserMapper.INSTANCE.createUserCmdToUser(cmd);
List<Role> roles = new ArrayList<>();
List<Role> role = new ArrayList<>();
Set<Role> ro = new HashSet<>();
roles = roleDao.findAll();
role.add(roles.get(0));
ro.addAll(role);
try {
user.setRoles(ro);
user.setPassword(passwordEncoder.encode(cmd.getPassword()));
user = userDAO.save(user);
} catch (DAOException e) {
LOGGER.error(null, e);
throw new ServiceException(ErrorCode.ERR_GEN_001, "Saving of user failed!", e);
}
return user;
}
#Override
public List<UserResult> findAll() {
return UserMapper.INSTANCE.listUserToListUserResult(userDAO.findAll());
}
#Override
public UserInfo findById(Long id) {
return UserMapper.INSTANCE.userToUserInfo(userDAO.findOne(id));
}
#Override
public void addAccount(PayPalAccount payPalAccount, User user) throws ServiceException{
try{
payPalAccount.setUserID(user);
payPalAccountDAO.save(payPalAccount);
} catch (DAOException e){
LOGGER.error(null, e);
throw new ServiceException(ErrorCode.ERR_GEN_001, "creating account failed");
}
}
#Override
public void addCart(ShoppingCart shoppingCart, User user) throws ServiceException {
try{
shoppingCart.setUser(user);
shoppingCart.setStatus(Status.NEW);
shoppingCart.setPrice(new BigDecimal(0));
shoppingCartDAO.save(shoppingCart);
} catch (DAOException e) {
LOGGER.error(null, e);
throw new ServiceException(ErrorCode.ERR_GEN_001, "creating cart failed ");
}
}
#Override
public void addRole(addRoleCmd cmd) throws ServiceException {
User user;
try{
user = userDAO.findOne(cmd.getId());
if(user == null){
throw new ServiceException(ErrorCode.ERR_GEN_002);
}
UserMapper.INSTANCE.addingRoletoUser(user, cmd);
user.getRoles().addAll(cmd.getRoles().stream()
.map(v ->{
Role rr = roleDao.findOne(v.getId());
rr.getUser().add(user);
return rr;
}).collect(Collectors.toSet()));
userDAO.merge(user);
}catch (DAOException e){
LOGGER.error(null, e);
throw new ServiceException(ErrorCode.ERR_GEN_001, "failed while adding new role", e);
}
}
#Override
public void update(UpdateUserCmd cmd) throws ServiceException {
User user;
try {
// check if entity still exists
user = userDAO.findOne(cmd.getId());
if (user == null) {
throw new ServiceException(ErrorCode.ERR_GEN_002);
}
UserMapper.INSTANCE.updateUserCmdToUser(user, cmd);
PayPalAccount palAccount = cmd.getPayPalAccount();
Set<ShoppingCart> shoppingCarts = cmd.getShoppingCarts();
for (ShoppingCart cart: shoppingCarts) {
addCart(cart, user);
}
user.setAccount(palAccount);
addAccount(palAccount, user);
userDAO.merge(user);
} catch (DAOException e) {
LOGGER.error(null, e);
throw new ServiceException(ErrorCode.ERR_GEN_001, "Update of user failed!", e);
}
}
#Override
public void delete(Long id) throws ServiceException {
User user = userDAO.findOne(id);
if (user != null) {
try {
userDAO.delete(user);
} catch (DAOException e) {
LOGGER.error(null, e);
throw new ServiceException(ErrorCode.ERR_GEN_001, "Delete of user failed!", e);
}
} else {
throw new ServiceException(ErrorCode.ERR_CAT_001, "User does not exist!");
}
}
#Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userDAO.findByUsername(username);
if(user == null){
LOGGER.error("User not found");
throw new UsernameNotFoundException("User not found in the database");
} else{
LOGGER.info("User found in the DB");
}
Collection<SimpleGrantedAuthority> authorities = new HashSet<>();
user.getRoles().forEach(role -> {
authorities.add(new SimpleGrantedAuthority(role.getName()));
});
return new org.springframework.security.core.userdetails.User(user.getUsername(), user.getPassword(), authorities);
}
}
Any suggestions?
The problem here is that if you read the documentation for WebMvcTest it says straight out in the second paragraph:
Using this annotation will disable full auto-configuration and instead apply only configuration relevant to MVC tests (i.e. #Controller, #ControllerAdvice, #JsonComponent, Converter/GenericConverter, Filter, WebMvcConfigurer and HandlerMethodArgumentResolver beans but not #Component, #Service or #Repository beans).
Which means it will only load a subsection of the application.
The code provided shows
#WebMvcTest(controllers = CategoryRest.class)
Which will only load the defined controller and the rest defined in the documentation.
The UserDetailsService is annotated as a #Service which means it will NOT be loaded at startup.
If you want to load the application fully you need to use #SpringBootTest in conjuction with other annotations for instance #AutoConfigureMockMvc or #AutoConfigureWebTestClientdepending on which client to use.
All of this is properly documentated with easy to read instructions in the spring boot documentation testing chapter.
Inside getUserObject() method we are not able to get Authentication object. It's available for 1st request only. But its setting to null for subsequent requests from client. So please help me to configure it properly so that its available for all the requests calls.
I am not sure how to configure inside configure method in AuthConfig.java so that authentication object would be available for all the requests chain
AuthConfig.java:
#Configuration
#EnableWebSecurity
public class AuthConfig extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable();
http.authorizeRequests()
.antMatchers("/callback", "/", "/auth0/authorize", "/resources/**", "/public/**", "/static/**",
"/login.do", "/logout.do", "/thankYou.do", "/customerEngagement.do",
"/oldCustomerEngagement.do", "/registerNew.do", "/forgotPassword.do", "/checkMongoService.do",
"/reset.do", "/rlaLogin.do", "/fnfrefer.do", "/thankYouLeadAggregator.do", "/referral")
.permitAll()
.anyRequest().authenticated().and().
logout()
.logoutUrl("/logout").logoutSuccessHandler(logoutSuccessHandler());
}
------------------------------------------------------------------------------
AuthController.java:
#RequestMapping(value = "/callback", method = RequestMethod.GET)
public void callback(HttpServletRequest request, HttpServletResponse response)
throws IOException, IdentityVerificationException {
try {
Tokens tokens = authenticationController.handle(request, response);
DecodedJWT jwt = JWT.decode(tokens.getIdToken());
List<GrantedAuthority> grantedAuths = new ArrayList<GrantedAuthority>();
grantedAuths.add(new SimpleGrantedAuthority("ROLE_USER"));
Authentication auth = new UsernamePasswordAuthenticationToken(jwt.getSubject(), jwt.getToken(), grantedAuths);
SecurityContext context = SecurityContextHolder.getContext();
context.setAuthentication(auth);
response.sendRedirect(config.getContextPath(request) + "/loancenter/home.do");
} catch (Exception e) {
LOG.info("callback page error");
response.sendRedirect(config.getContextPath(request) + "/loancenter");
}
}
--------------------------------------------------------------------------------
HomeController.java:
#Controller
public class DefaultController implements InitializingBean {
#RequestMapping(value = "home.do")
public ModelAndView showCustomerPage(HttpServletRequest req, HttpServletResponse res, Model model) {
ModelAndView mav = new ModelAndView();
try {
User user = getUserObject(req);
if(user==null) {
LOG.info("User not found in session");
mav.setViewName(JspLookup.LOGIN);
return mav;
}
} catch (Exception e) {
LOG.error("Exception in Home page ", e);
}
return mav;
}
protected User getUserObject(HttpServletRequest request) {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
LOG.info("authentication::{}", authentication);
User user = null;
if (authentication == null) {
return user;
}
if (authentication.getPrincipal() instanceof User) {
user = (User) authentication.getPrincipal();
LOG.info("User already authenticated and logging :{}", user.getEmailId());
sendUserLoginEmailToLO(user);
} else {
UsernamePasswordAuthenticationToken token = (UsernamePasswordAuthenticationToken) authentication;
DecodedJWT jwt = JWT.decode(token.getCredentials().toString());
user = userProfileDao.findByUserEmail(jwt.getClaims().get("email").asString());
if (user != null) {
LOG.info("First time authentication:{}", user.getEmailId());
boolean auth0EmailVerified = jwt.getClaims().get("email_verified").asBoolean();
LOG.info("First time authentication email verified flag from auth0:{}", auth0EmailVerified);
LOG.info("First time authentication email verified flag from nlc:{}", user.getEmailVerified());
if (BooleanUtils.isFalse(user.getEmailVerified()) && auth0EmailVerified) {
LOG.info("Email is verified in Auth0, updating email_verified flag to true in DB for userId: {}",
user.getId());
userProfileDao.verifyEmail(user.getId());
LOG.info("First time authentication updated email verified flag in nlc db:{}", user.getEmailId());
}
if (user.getNewEmailVerified() != null && BooleanUtils.isFalse(user.getNewEmailVerified())) {
LOG.info("The user is verifying his email: set his verified to true");
userProfileDao.verifyNewEmail(user.getId());
}
Authentication auth = new UsernamePasswordAuthenticationToken(user, jwt.getToken(),
token.getAuthorities());
messageServiceHelper.checkIfUserFirstLogin(user);
LOG.info("Authentication provided for user : {}", user.getEmailId());
LOG.debug("Auth object constructed : {}", auth);
SecurityContextHolder.getContext().setAuthentication(auth);
HttpSession session = request.getSession(true);
session.setAttribute("SPRING_SECURITY_CONTEXT", SecurityContextHolder.getContext());
sendUserLoginEmailToLO(user);
}
}
return user;
}
}
I use spring boot with spring cloud gateway
I have another app with spring boot and thymeleaf
Spring gateway return a token to my thymeleaf app.
#EnableWebFluxSecurity
#Configuration
public class WebFluxSecurityConfig {
#Autowired
private WebFluxAuthManager authManager;
#Bean
protected SecurityWebFilterChain securityFilterChange(ServerHttpSecurity http) throws Exception {
http.authorizeExchange()
// URL that starts with / or /login/
.pathMatchers("/", "/login", "/js/**", "/images/**", "/css/**", "/h2-console/**").permitAll()
.anyExchange().authenticated().and().formLogin()
.authenticationManager(authManager)
.authenticationSuccessHandler(new RedirectServerAuthenticationSuccesHandler("/findAllCustomers"));
return http.build();
}
}
WebFluxAuthManager class
#Component
public class WebFluxAuthManager implements ReactiveAuthenticationManager {
#Value("${gateway.url}")
private String gatewayUrl;
#Override
public Mono<Authentication> authenticate(Authentication authentication) {
// return is already authenticated
if (authentication.isAuthenticated()) {
return Mono.just(authentication);
}
String username = authentication.getName();
String password = authentication.getCredentials().toString();
LoginRequest loginRequest = new LoginRequest(username, password);
CloseableHttpClient httpClient = HttpClients.createDefault();
try {
//todo modify to use webclient
HttpPost httpPost = new HttpPost(this.gatewayUrl + "/authenticate");
httpPost.setHeader("Content-type", "application/json");
String jsonReq = converObjectToJson(loginRequest);
StringEntity requestEntity = new StringEntity(jsonReq);
httpPost.setEntity(requestEntity);
CloseableHttpResponse httpResponse = httpClient.execute(httpPost);
if (httpResponse.getStatusLine().getStatusCode() == HttpStatus.OK.value()) {
HttpEntity entity = httpResponse.getEntity();
Header encodingHeader = entity.getContentEncoding();
Charset encoding = encodingHeader == null ? StandardCharsets.UTF_8
: Charsets.toCharset(encodingHeader.getValue());
// use org.apache.http.util.EntityUtils to read json as string
String jsonRes = EntityUtils.toString(entity, encoding);
LoginResponse loginResponse = converJsonToResponse(jsonRes);
Collection<? extends GrantedAuthority> authorities = loginResponse.getRoles().stream()
.map(item -> new SimpleGrantedAuthority(item)).collect(Collectors.toList());
return Mono.just(new UsernamePasswordAuthenticationToken(username, password, authorities));
} else {
throw new BadCredentialsException("Authentication Failed!!!");
}
} catch (RestClientException | ParseException | IOException e) {
throw new BadCredentialsException("Authentication Failed!!!", e);
} finally {
try {
if (httpClient != null)
httpClient.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
In WebFluxAuthManager, I have access to the token, now I search a way to transfert it to a fragment.
We have login page where user will enter user credentials and internally calling one more authenticate service where need to store this token and pass to all REST controllers.I tried configuring bean scope within this class and but getting below exception .we are using spring 5.x;
com.config.CustomAuthenticationProvider sessionScopedBean
CustomAuthenticationProvider UserDetails !!!null
Jun 20, 2020 11:52:37 AM org.apache.catalina.core.StandardWrapperValve invoke
java.lang.ClassCastException: org.springframework.beans.factory.support.NullBean cannot be cast to com.utils.UserDetails
#Component
public class CustomAuthenticationProvider implements AuthenticationProvider {
private Logger logger = Logger.getLogger(getClass().getName());
private UserDetails userDetails;
#Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
String userName = authentication.getName();
String passWord = authentication.getCredentials().toString();
Result response;
try {
response = CustomClient.authenticate(userName, passWord);
} catch (Exception e) {
throw new BadCredentialsException("system authentication failed");
}
if (response != null && response.getToken() != null) {
//need to store this response.getToken() in session
logger.info("Token: " + response.getToken());
userDetails= new UserDetails();
userDetails.setToken(response.getToken());
logger.info("Authentication SUCCESS !!!");
return new UsernamePasswordAuthenticationToken(userName, passWord, Collections.emptyList());
} else {
logger.info("Authentication FAILED...");
throw new BadCredentialsException("authentication failed");
}
}
#Bean
#Scope(value = WebApplicationContext.SCOPE_SESSION, proxyMode = ScopedProxyMode.TARGET_CLASS)
public UserDetails sessionScopedBean() {
logger.info(" UserDetails !!!"+userDetails);
return userDetails;
}
#Override
public boolean supports(Class<?> auth) {
return auth.equals(UsernamePasswordAuthenticationToken.class);
}
}
Why do you want to create session scoped UserDetails bean in the first place? You can already achieve it by doing the following:
#GetMapping("/abc")
public void getUserProfile(#AuthenticationPrincipal UserDetails user ) {
...
}
or
#GetMapping("/abc")
public void getUserProfile() {
SecurityContext securityContext = SecurityContextHolder.getContext();
UserDetails user = (UserDetails) securityContext.getAuthentication().getPrincipal();
}
Note:
Behind the scene, spring uses HttpSessionSecurityContextRepository to store your SecurityContext in http session and restore it back on every request
And the Updated CustomAuthenticationProvider
#Component
public class CustomAuthenticationProvider implements AuthenticationProvider {
private Logger logger = Logger.getLogger(getClass().getName());
#Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
String userName = authentication.getName();
String passWord = authentication.getCredentials().toString();
Result response;
try {
response = CustomClient.authenticate(userName, passWord);
} catch (Exception e) {
throw new BadCredentialsException("system authentication failed");
}
if (response != null && response.getToken() != null) {
//need to store this response.getToken() in session
logger.info("Token: " + response.getToken());
UserDetails userDetails= new UserDetails();
userDetails.setToken(response.getToken());
logger.info("Authentication SUCCESS !!!");
return new UsernamePasswordAuthenticationToken(userDetails, passWord, Collections.emptyList());
} else {
logger.info("Authentication FAILED...");
throw new BadCredentialsException("authentication failed");
}
}
#Override
public boolean supports(Class<?> auth) {
return auth.equals(UsernamePasswordAuthenticationToken.class);
}
}
First of all you can not create Bean like your example. #Bean annotation is processed when application context is starting. UserDetails will be null so it can not be created.
You are creating UserDetails after applacation context is up.
Do you really want to keep session if that's case
#Configuration
public class Config {
#Bean
#Scope(value = WebApplicationContext.SCOPE_SESSION, proxyMode = ScopedProxyMode.TARGET_CLASS)
public UserDetails userDetails() {
return new UserDetails();
}
}
#Component
public class CustomAuthenticationProvider implements AuthenticationProvider {
private Logger logger = Logger.getLogger(getClass().getName());
#Autowired
private UserDetails userDetails;
}
You can inject by Autowire or constructor injection
Don't instantiate it manually just inject it and use in method like below
userDetails.setToken(response.getToken());
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.