Tuesday, April 24, 2018

Spring Boot Social Login and Local OAuth2-Server

Leave a Comment

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! :)

1 Answers

Answers 1

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.

If You Enjoyed This, Take 5 Seconds To Share It

0 comments:

Post a Comment