Spring boot Security, Oauth2 replace access token with long-lived token from facebook - spring

I have been following Dave Syer astounding tutorial to implement OAuth2 in microservices which provide RESTful APIs for mobile devices (Android and iOS). I have configured gateway security with the following code:
#SpringBootApplication
#EnableDiscoveryClient
#EnableZuulProxy
#EnableCircuitBreaker
#EnableFeignClients
#EnableOAuth2Client
public class GatewayApplication extends WebSecurityConfigurerAdapter {
private OAuth2ClientContext oauth2ClientContext;
private SimpleUrlAuthenticationSuccessHandler simpleUrlAuthenticationSuccessHandler;
private ScoreAuthorizationFilter scoreAuthorizationFilter;
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf().disable()
.antMatcher("/**")
.authorizeRequests()
.antMatchers("/", "/test", "/login**", "/webjars/**", "/error**")
.permitAll()
.anyRequest()
.authenticated()
.and().logout().logoutSuccessUrl("/").permitAll()
.and().addFilterBefore(ssoFilter(), BasicAuthenticationFilter.class)
.addFilterBefore(scoreAuthorizationFilter, BasicAuthenticationFilter.class)
;
}
private Filter ssoFilter() {
OAuth2ClientAuthenticationProcessingFilter facebookFilter = new OAuth2ClientAuthenticationProcessingFilter("/login/facebook");
OAuth2RestTemplate facebookTemplate = new OAuth2RestTemplate(facebook(), oauth2ClientContext);
facebookFilter.setRestTemplate(facebookTemplate);
UserInfoTokenServices tokenServices = new UserInfoTokenServices(facebookResource().getUserInfoUri(), facebook().getClientId());
tokenServices.setRestTemplate(facebookTemplate);
facebookFilter.setTokenServices(tokenServices);
facebookFilter.setAuthenticationSuccessHandler(simpleUrlAuthenticationSuccessHandler);
return facebookFilter;
}
#Bean
#ConfigurationProperties("facebook.client")
public AuthorizationCodeResourceDetails facebook() {
return new AuthorizationCodeResourceDetails();
}
#Bean
#ConfigurationProperties("facebook.resource")
public ResourceServerProperties facebookResource() {
return new ResourceServerProperties();
}
#Bean
public FilterRegistrationBean<OAuth2ClientContextFilter> oauth2ClientFilterRegistration(OAuth2ClientContextFilter filter) {
FilterRegistrationBean<OAuth2ClientContextFilter> registration = new FilterRegistrationBean<OAuth2ClientContextFilter>();
registration.setFilter(filter);
registration.setOrder(-100);
return registration;
}
#Bean
public RequestInterceptor getFeignClientInterceptor() {
return new FeignClientInterceptor();
}
}
It turns out that the user's session expires after a while. As I dug a little deeper, I found out that Facebook doesn't provide refresh tokens. Instead, we can exchange a short-lived token for a long-lived token (Facebook long-lived token). How can I override the standard OAuth2 flow implemented in Spring Security to send another request to Facebook for getting the long-lived token and then replacing the old access token?

You can achieve what you want by extending the OAuth2ClientAuthenticationProcessingFilter class like this:
public class CustomAuthenticationProcessingFilter extends OAuth2ClientAuthenticationProcessingFilter {
private ResourceServerTokenServices tokenServices;
private AuthenticationDetailsSource<HttpServletRequest, ?> authenticationDetailsSource = new OAuth2AuthenticationDetailsSource();
private ApplicationEventPublisher eventPublisher;
private AuthorizationCodeResourceDetails facebook;
private String longLivedTokenUri;
public CustomAuthenticationProcessingFilter(String defaultFilterProcessesUrl) {
super(defaultFilterProcessesUrl);
setAuthenticationDetailsSource(authenticationDetailsSource);
}
#Override
public void setTokenServices(ResourceServerTokenServices tokenServices) {
this.tokenServices = tokenServices;
super.setTokenServices(tokenServices);
}
#Override
public void setApplicationEventPublisher(ApplicationEventPublisher eventPublisher) {
this.eventPublisher = eventPublisher;
super.setApplicationEventPublisher(eventPublisher);
}
#Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
throws AuthenticationException {
OAuth2AccessToken accessToken;
try {
accessToken = restTemplate.getAccessToken();
} catch (OAuth2Exception e) {
BadCredentialsException bad = new BadCredentialsException("Could not obtain access token", e);
publish(new OAuth2AuthenticationFailureEvent(bad));
throw bad;
}
String longLivedToken = getFromFacebook(); //Get long lived token from facebook here
try {
OAuth2Authentication result = tokenServices.loadAuthentication(longLivedToken);
if (authenticationDetailsSource != null) {
request.setAttribute(OAuth2AuthenticationDetails.ACCESS_TOKEN_VALUE, longLivedToken);
request.setAttribute(OAuth2AuthenticationDetails.ACCESS_TOKEN_TYPE, accessToken.getTokenType());
result.setDetails(authenticationDetailsSource.buildDetails(request));
}
publish(new AuthenticationSuccessEvent(result));
return result;
} catch (InvalidTokenException e) {
BadCredentialsException bad = new BadCredentialsException("Could not obtain user details from token", e);
publish(new OAuth2AuthenticationFailureEvent(bad));
throw bad;
}
}
private void publish(ApplicationEvent event) {
if (eventPublisher != null) {
eventPublisher.publishEvent(event);
}
}
}
I hope this helps.

Related

Spring Security For Authorities Based Filtering Using Custom Http Header

I am trying to implement RBAC using Spring Security. User authentication is implemented separately and sessionId is generated for the app to use. I wanted to have Spring Security take the sessionId from the Http Header and would use the sessionId to get the Authorities from a database to determine whether the user is authorized to access certain endpoints. The problem is that I don't know how to get the authorities from the database on demand and I don't know if the configuration is being done correctly. This is what I have so far:
#Configuration
#EnableWebSecurity
public class CustomSecurityFilter {
#Bean
AuthenticationManager customAuthenticationManager(HttpHeaderAuthenticationProvider httpHeaderAuthenticationProvider) {
return new ProviderManager(List.of(httpHeaderAuthenticationProvider));
}
#Bean
HttpHeaderAuthenticationProvider newHttpHeaderAuthenticationProvider() {
return new HttpHeaderAuthenticationProvider();
}
#Bean
public SecurityFilterChain filterChain(HttpSecurity http,
AuthenticationManager authenticationManager) throws Exception {
http.addFilterBefore(getFilter(authenticationManager), AnonymousAuthenticationFilter.class).authorizeRequests()
.antMatchers(HttpMethod.GET, "/api/apples").hasAuthority("viewApples")
.antMatchers(HttpMethod.POST, "/api/apples").hasAuthority("createApples")
return http.build();
}
private Filter getFilter(AuthenticationManager authenticationManager) {
return new HttpHeaderProcessingFilter(
new OrRequestMatcher(
new AntPathRequestMatcher("/api/apples/**"),
),
authenticationManager
);
}
}
public class HttpHeaderAuthenticationProvider implements AuthenticationProvider {
#Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
var sessionId = ((String) authentication.getPrincipal());
// Somehow connect to database to get session and authorities information?
boolean isValid = sessionId != null;
if (isValid) {
return newPreAuthenticatedToken("sessionId", List.of());
} else {
throw new AccessDeniedException("Invalid sessionId");
}
}
#Override
public boolean supports(Class<?> authentication) {
return PreAuthenticatedAuthenticationToken.class.equals(authentication);
}
public static PreAuthenticatedAuthenticationToken newPreAuthenticatedToken(String userId, List<String> permissions) {
var grantedAuthorityList = new ArrayList<GrantedAuthority>();
for (String permission : permissions) {
grantedAuthorityList.add(new SimpleGrantedAuthority(permission));
}
return new PreAuthenticatedAuthenticationToken(userId, null, grantedAuthorityList);
}
}
public class HttpHeaderProcessingFilter extends AbstractAuthenticationProcessingFilter {
public HttpHeaderProcessingFilter(RequestMatcher requiresAuthenticationRequestMatcher,
AuthenticationManager authenticationManager) {
super(requiresAuthenticationRequestMatcher);
setAuthenticationManager(authenticationManager);
}
#Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
throws AuthenticationException {
return getAuthenticationManager().authenticate(
// Not sure if we are supposed to do this
HttpHeaderAuthenticationProvider.newPreAuthenticatedToken("sessionId", List.of())
);
}
#Override
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain,
Authentication authResult) throws IOException, ServletException {
SecurityContextHolder.getContext().setAuthentication(authResult);
chain.doFilter(request, response);
}
}
I tried using these resources:
https://salahuddin-s.medium.com/custom-header-based-authentication-using-spring-security-17f4163d0986
https://www.baeldung.com/spring-security-granted-authority-vs-role
I was also wondering whether JWT would be a good candidate to use in place of a custom sessionId with RBAC + Session Handling.
I was able to configure the filter to use authorities. Here is what I have:
#Component
#Slf4j
public class CustomPreAuthProvider implements AuthenticationProvider {
private boolean throwExceptionWhenTokenRejected;
#Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
if (!this.supports(authentication.getClass())) {
return null;
} else {
log.debug(String.valueOf(LogMessage.format("PreAuthenticated authentication request: %s", authentication)));
if (authentication.getPrincipal() == null) {
log.debug("No pre-authenticated principal found in request.");
if (this.throwExceptionWhenTokenRejected) {
throw new BadCredentialsException("No pre-authenticated principal found in request.");
} else {
return null;
}
} else if (authentication.getCredentials() == null) {
log.debug("No pre-authenticated credentials found in request.");
if (this.throwExceptionWhenTokenRejected) {
throw new BadCredentialsException("No pre-authenticated credentials found in request.");
} else {
return null;
}
} else if (!authentication.isAuthenticated()) {
throw new InsufficientAuthenticationException("Access token likely no longer valid.");
}
return authentication;
}
}
#Override
public boolean supports(Class<?> authentication) {
return authentication.equals(PreAuthenticatedAuthenticationToken.class);
}
public void setThrowExceptionWhenTokenRejected(boolean throwExceptionWhenTokenRejected) {
this.throwExceptionWhenTokenRejected = throwExceptionWhenTokenRejected;
}
}
#Service
public class CustomUserDetails implements UserDetailsService {
#Autowired
private SessionRepository sessionRepository;
#Autowired
private RoleRepository roleRepository;
#Autowired
private AuthHelper authHelper;
#Override
public UserDetails loadUserByUsername(String sessionId) throws UsernameNotFoundException, IllegalStateException {
var sessions = sessionRepository.getSession(sessionId); // Database query for session information
if (sessions == null || sessions.isEmpty()) {
throw new UsernameNotFoundException("Session Not Found");
} else if (sessions.size() > 1) {
throw new IllegalStateException("More than one record with sessionId found");
}
var session = sessions.get(0);
var authoritySet = new HashSet<String>();
for (String role : session.getRoles()) {
var authorities = roleRepository.getUserPrivilegesByRoleName(role); // Database query for authorities
for (UserRolePrivilege userRolePrivilege : authorities) {
authoritySet.add(userRolePrivilege.getPermittedAction());
}
}
var grantedAuthority = new ArrayList<GrantedAuthority>();
for (String authority : authoritySet) {
grantedAuthority.add(new SimpleGrantedAuthority(authority));
}
var introspect = authHelper.validateAccessToken(session.getSessionId(), session.getAccessToken(),
session.getRefreshToken(), session.getExpirationTime()); // Code to verify token
var user = new UserImpl();
user.setUsername(session.getEmail());
user.setPassword(session.getAccessToken());
user.setEnabled(introspect.getIntrospect().isActive());
user.setAccountNonExpired(introspect.getIntrospect().isActive());
user.setAccountNonLocked(introspect.getIntrospect().isActive());
user.setCredentialsNonExpired(introspect.getIntrospect().isActive());
user.setAuthorities(grantedAuthority);
return user;
}
}
public class SessionAuthFilter extends AbstractAuthenticationProcessingFilter {
private final CustomUserDetails customUserDetails;
protected SessionAuthFilter(RequestMatcher requestMatcher, AuthenticationManager authenticationManager,
CustomUserDetails customUserDetails) {
super(requestMatcher, authenticationManager);
this.customUserDetails = customUserDetails;
this.setContinueChainBeforeSuccessfulAuthentication(true);
this.setAuthenticationSuccessHandler((request, response, authentication) -> {});
}
#Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
throws AuthenticationException {
var sessionId = request.getHeader("sessionId") != null ? request.getHeader("sessionId").trim() : null;
var user = customUserDetails.loadUserByUsername(sessionId);
var authentication = new PreAuthenticatedAuthenticationToken(user.getUsername(), user.getPassword(),
user.getAuthorities());
authentication.setAuthenticated(user.isCredentialsNonExpired());
authentication.setDetails(customUserDetails);
SecurityContextHolder.getContext().setAuthentication(authentication);
return this.getAuthenticationManager().authenticate(authentication);
}
}
#Configuration
#EnableWebSecurity
public class SecurityConfig {
#Bean
AuthenticationManager customAuthenticationManager(CustomPreAuthProvider preAuthProvider) {
return new ProviderManager(List.of(preAuthProvider));
}
#Bean
SessionAuthFilter customAuthFilter(AuthenticationManager authManager, CustomUserDetails customUserDetails) {
return new SessionAuthFilter(
new OrRequestMatcher(
new AntPathRequestMatcher("/apples/**"),
),
authManager,
customUserDetails);
}
#Bean
public SecurityFilterChain filterChain(HttpSecurity http, SessionAuthFilter authFilter) throws Exception {
http.exceptionHandling()
.authenticationEntryPoint(new Http403ForbiddenEntryPoint())
.accessDeniedHandler(new AccessDeniedHandlerImpl())
.and()
.formLogin().disable()
.httpBasic().disable()
.authorizeRequests()
.antMatchers(
"/",
"/error",
"/v3/api-docs/**",
"/swagger-ui/**",
"/swagger-ui.html",
"/actuator/**"
).permitAll()
.antMatchers(HttpMethod.GET, "/apples").hasAuthority("viewApples")
.antMatchers(HttpMethod.POST, "/apples").hasAuthority("createApples")
.anyRequest().authenticated()
.and()
.addFilterBefore(authFilter, AbstractPreAuthenticatedProcessingFilter.class)
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
return http.build();
}
}

Spring Security Redirecting After Successful Authentication

I am trying to add access control to a set of api endpoints and the problem I am running into is that the service is redirecting to / regardless of whether the original request was /api/apple or /api/orange. I currently have a filter set up to read a custom http header to do the authentication and the filter I am using is extended from AbstractAuthenticationProcessingFilter. The documentation is saying that it is intended for the AbstractAuthenticationProcessingFilter to redirect to a specific url upon successful authentication, but this is not the behavior I want for an api. I think I may be using the wrong Filter, but I don't know which one I should be using. Can I get some help on what I may be doing wrong and what I should be doing?
Filter Chain Configuration:
#Configuration
#EnableWebSecurity
public class SecurityConfig {
#Bean
AuthenticationManager customAuthenticationManager(PreAuthProvider preAuthProvider) {
return new ProviderManager(List.of(preAuthProvider));
}
#Bean
SessionAuthFilter customAuthFilter(AuthenticationManager authManager, CustomUserDetails userDetails) {
return new SessionAuthFilter(
new OrRequestMatcher(
new AntPathRequestMatcher("/apple/**"),
new AntPathRequestMatcher("/orange/**")
),
authManager,
userDetails);
}
#Bean
public SecurityFilterChain filterChain(HttpSecurity http, SessionAuthFilter authFilter) throws Exception {
http.exceptionHandling()
.authenticationEntryPoint(new Http403ForbiddenEntryPoint())
.accessDeniedHandler(new AccessDeniedHandlerImpl())
.and()
.formLogin().disable()
.httpBasic().disable()
.authorizeRequests()
.antMatchers(
"/",
"/error",
"/v3/api-docs/**",
"/swagger-ui/**",
"/swagger-ui.html",
"/actuator/**"
).permitAll()
.antMatchers(GET, "/apple").hasAuthority("getApples")
.antMatchers(GET, "/orange").hasAuthority("getOranges")
.anyRequest().authenticated()
.and()
.addFilterBefore(authFilter, AbstractPreAuthenticatedProcessingFilter.class)
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
return http.build();
}
Filter Implementation:
public class SessionAuthFilter extends AbstractAuthenticationProcessingFilter {
private final CustomUserDetails userDetails;
protected SessionAuthFilter(RequestMatcher requestMatcher, AuthenticationManager authenticationManager,
CustomUserDetails userDetails) {
super(requestMatcher, authenticationManager);
this.userDetails = userDetails;
}
#Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
throws AuthenticationException {
var sessionToken = request.getHeader("SessionToken") != null ? request.getHeader("SessionToken").trim() : null;
var user = userDetails.loadUserByUsername(sessionToken);
var authentication = new PreAuthenticatedAuthenticationToken(user.getUsername(), user.getPassword(),
user.getAuthorities());
authentication.setAuthenticated(user.isCredentialsNonExpired());
authentication.setDetails(userDetails);
SecurityContextHolder.getContext().setAuthentication(authentication);
return this.getAuthenticationManager().authenticate(authentication);
}
}
Authentication Provider:
#Component
#Slf4j
public class PreAuthProvider implements AuthenticationProvider {
private boolean throwExceptionWhenTokenRejected;
#Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
if (!this.supports(authentication.getClass())) {
return null;
} else {
log.debug(String.valueOf(LogMessage.format("PreAuthenticated authentication request: %s", authentication)));
if (authentication.getPrincipal() == null) {
log.debug("No pre-authenticated principal found in request.");
if (this.throwExceptionWhenTokenRejected) {
throw new BadCredentialsException("No pre-authenticated principal found in request.");
} else {
return null;
}
} else if (authentication.getCredentials() == null) {
log.debug("No pre-authenticated credentials found in request.");
if (this.throwExceptionWhenTokenRejected) {
throw new BadCredentialsException("No pre-authenticated credentials found in request.");
} else {
return null;
}
} else if (!authentication.isAuthenticated()) {
throw new InsufficientAuthenticationException("Session token likely no longer valid.");
}
return authentication;
}
}
#Override
public boolean supports(Class<?> authentication) {
return authentication.equals(PreAuthenticatedAuthenticationToken.class);
}
public void setThrowExceptionWhenTokenRejected(boolean throwExceptionWhenTokenRejected) {
this.throwExceptionWhenTokenRejected = throwExceptionWhenTokenRejected;
}
}
It looks like if you set continueChainBeforeSuccessfulAuthentication in your AbstractAuthenticationProcessingFilter implementation to true, you can delay the redirection. Using your own success handler implementation will completely stop the redirect behavior. I only needed to modify the filter constructor which came out to be:
protected SessionAuthFilter(RequestMatcher requestMatcher, AuthenticationManager authenticationManager,
CustomUserDetails userDetails) {
super(requestMatcher, authenticationManager);
this.userDetails = userDetails;
this.setContinueChainBeforeSuccessfulAuthentication(true);
this.setAuthenticationSuccessHandler((request, response, authentication) -> {});
}
The other approach would be to implement a different Filter such as OncePerRequestFilter or a GenericFilterBean to handle the authentication yourself.

spring boot security custom successHandler with rest not working

not sure if my question is good..
Perhaps I was looking very badly for information about the spring security
In general, I hope it will not be difficult for you to answer.
The question is, I use spring security with my login page. The login page is just in the public templates folder. I do not create a separate Controller for it that would return the view page (would it be correct to create a controller for it that would return the view login page?). In any case, my code works even without this page view controller. But only my custom SuccessHandler does not work (which, after login, checks by roles and would redirect to another page).
Should I redirect by role to the appropriate pages using a different approach? (I mean if ADMIN_ROLE after login is redirected to the admin-panel.html)
my security
#Configuration
#EnableWebSecurity
#EnableGlobalMethodSecurity(securedEnabled = true)
public class CustomWebSecurityConfiguration extends WebSecurityConfigurerAdapter {
#Autowired
private UserServiceImpl userServiceImpl;
#Override
protected void configure(HttpSecurity http) throws Exception {
http.cors().and()
.authorizeRequests()
.antMatchers("/", "/templates/sign-up.html").permitAll()
.antMatchers("/api/users", "/api/users/login").permitAll()
.antMatchers("/templates/admin-panel.html").hasRole("ADMIN")
.antMatchers("/all-users").hasRole("ADMIN")
.antMatchers("/news").hasRole("USER")
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/templates/login.html")
.defaultSuccessUrl("/")
.permitAll()
.successHandler(myAuthenticationSuccessHandler())
.and()
.logout()
.permitAll()
.logoutSuccessUrl("/index.html");
http.csrf().disable();
}
#Override
public void configure(WebSecurity web) {
web
.ignoring()
.antMatchers("/css/**")
.antMatchers("/js/**")
.antMatchers("/static/**")
.antMatchers("/resources/**");
}
#Autowired
protected void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userServiceImpl).passwordEncoder(bCryptPasswordEncoder());
}
#Bean
public AuthenticationSuccessHandler myAuthenticationSuccessHandler(){
return new CustomAuthenticationSuccessHandler();
}
#Bean
#Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
#Bean
public BCryptPasswordEncoder bCryptPasswordEncoder() {
return new BCryptPasswordEncoder();
}
my custom success handler
public class CustomAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
protected final Log logger = LogFactory.getLog(this.getClass());
private RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();
public CustomAuthenticationSuccessHandler() {
super();
}
// API
#Override
public void onAuthenticationSuccess(final HttpServletRequest request, final HttpServletResponse response, final Authentication authentication) throws IOException {
handle(request, response, authentication);
clearAuthenticationAttributes(request);
}
// IMPL
protected void handle(final HttpServletRequest request, final HttpServletResponse response, final Authentication authentication) throws IOException {
final String targetUrl = determineTargetUrl(authentication);
if (response.isCommitted()) {
logger.debug("Response has already been committed. Unable to redirect to " + targetUrl);
return;
}
redirectStrategy.sendRedirect(request, response, targetUrl);
}
protected String determineTargetUrl(final Authentication authentication) {
Map<String, String> roleTargetUrlMap = new HashMap<>();
roleTargetUrlMap.put("ROLE_USER", "/index.html");
roleTargetUrlMap.put("ROLE_ADMIN", "/templates/admin-panel.html");
final Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
for (final GrantedAuthority grantedAuthority : authorities) {
String authorityName = grantedAuthority.getAuthority();
if(roleTargetUrlMap.containsKey(authorityName)) {
return roleTargetUrlMap.get(authorityName);
}
}
throw new IllegalStateException();
}
/**
* Removes temporary authentication-related data which may have been stored in the session
* during the authentication process.
*/
protected final void clearAuthenticationAttributes(final HttpServletRequest request) {
final HttpSession session = request.getSession(false);
if (session == null) {
return;
}
session.removeAttribute(WebAttributes.AUTHENTICATION_EXCEPTION);
}
}
my controller
#CrossOrigin
#RestController
#RequestMapping("/api/users")
public class UserController {
private final UserServiceImpl userService;
private AuthenticationManager authenticationManager;
public UserController(UserServiceImpl userService, AuthenticationManager authenticationManager) {
this.userService = userService;
this.authenticationManager = authenticationManager;
}
#PostMapping
public ResponseEntity<?> register(#RequestBody UserDTO user) {
try {
userService.register(user);
return new ResponseEntity<>("User added", HttpStatus.OK);
} catch (Exception e) {
return new ResponseEntity<>(e, HttpStatus.BAD_REQUEST);
}
}
#PostMapping(value = "/login")
public ResponseEntity<?> login(#RequestBody UserDTO user, HttpServletResponse response) {
try {
Authentication authentication = authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(user.getUsername(), user.getPassword()));
boolean isAuthenticated = isAuthenticated(authentication);
if (isAuthenticated) {
SecurityContextHolder.getContext().setAuthentication(authentication);
// response.sendRedirect("/templates/admin-panel.html");
// my pathetic attempt to create a redirect to another page
}
return new ResponseEntity<>("user authenticated", HttpStatus.OK);
} catch (Exception e) {
return new ResponseEntity<>(e, HttpStatus.FORBIDDEN);
}
}
private boolean isAuthenticated(Authentication authentication) {
return authentication != null && !(authentication instanceof AnonymousAuthenticationToken) && authentication.isAuthenticated();
}
my static files
enter image description here
My Guess, as you didn't post your login page itself:
You don't need a controller listening to POST /login this normally automatically registered by Spring Security with all security related authentication stuff. No need to try it by yourself as in UserController.login(). I guess by regsitering this endpoint you override / deactivate the regular spring security behaviour.
Normally you just need a login page with a form that posts correctly to /login. The handling on the backend side is done by spring security itself.
See https://spring.io/guides/gs/securing-web/ for a minimal worling setup.

How to reuse oauth2 token from user (authorization_code) in a Rest Template

I have 3 applications
Frontend application
OAuth2 authentication server
REST api (RepositoryRestResources)
My users have to log in before being able to use the frontend application. This happens through SSO. They receive a token which is validated by the client before being let in.
I would like to reuse this token to make api requests. My REST api application is secured with the same SSO login (it is a resource for the frontend client) but I have no clue how to "add the authorization header" to be used in the RestTemplate I use for api requests.
I create my restTemplate like this:
public static RestTemplate build()
{
ObjectMapper mapper = new ObjectMapper();
mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
mapper.registerModule(new Jackson2HalModule());
mapper.registerModule(new JavaTimeModule());
MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter();
converter.setSupportedMediaTypes(MediaType.parseMediaTypes("application/hal+json"));
converter.setObjectMapper(mapper);
return new RestTemplate(Arrays.asList(converter));
}
my resource server configuration:
#Configuration
#EnableResourceServer
public class ResourceServerConfiguration extends ResourceServerConfigurerAdapter
{
#Value("${resource.id}")
private String resourceId;
#Override
public void configure(HttpSecurity http) throws Exception
{
http
.authorizeRequests()
.antMatchers(HttpMethod.OPTIONS).permitAll()
.anyRequest().authenticated()
.and().exceptionHandling().accessDeniedHandler(new OAuth2AccessDeniedHandler());
}
#Override
public void configure(ResourceServerSecurityConfigurer resources) throws Exception
{
resources.resourceId(resourceId);
}
#Bean
public static TokenEnhancer tokenEnhancer()
{
return new JwtTokenEnhancer();
}
#Bean
public static JwtAccessTokenConverter accessTokenConverter()
{
KeyStoreKeyFactory keyStoreKeyFactory = new KeyStoreKeyFactory(new ClassPathResource("keystore.jks"), "somesecret".toCharArray());
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
converter.setKeyPair(keyStoreKeyFactory.getKeyPair("pair"));
return converter;
}
#Bean
public static TokenStore tokenStore()
{
return new JwtTokenStore(accessTokenConverter());
}
}
I fixed it using an intercepter an manually adding the Token from the security context.
RestTemplate restTemplate = new RestTemplate();
restTemplate.getInterceptors().add(new OAuthInterceptor());
In which the intercepter is defined as:
public class OAuthInterceptor implements ClientHttpRequestInterceptor
{
#Autowired
private AuthenticationHolder holder;
#Override
public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException
{
if (holder.getToken() == null)
{
//throw new IOException("Token not set");
System.out.println("##################### Token not set! ###################");
}
else
{
System.out.println("##################### Token found: " + holder.getToken());
HttpHeaders headers = request.getHeaders();
headers.add(HttpHeaders.AUTHORIZATION, "Bearer " + holder.getToken());
}
return execution.execute(request, body);
}
}
I use an interface which I implement in my client app:
public interface AuthenticationHolder
{
String getToken();
}
#Bean
public AuthenticationHolder getAuthenticationHolder()
{
return () ->
{
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if(authentication != null && authentication.getDetails() instanceof OAuth2AuthenticationDetails)
{
return ((OAuth2AuthenticationDetails) authentication.getDetails()).getTokenValue();
}
return null;
};
}
You can use the #PreAuthorize(ROLE) on the top of your methods, so when this method is called he will as for the Token then he will check if the provided Token has the necessary ROLE to use the method.
Of course that you will need to configure your API to connect to your OAuth Database.
Example:
#PreAuthorize("ROLE_ADMIN")
public void deleteAll(){
...
}

Custom Spring Security OAuth2 with Spring Social integration

Custom Spring security OAuth2 is working fine and now would like to add Spring Social integration(facebook login, google login etc), When the user clicks on Facebook login(user would not provide any username/password), Facebook will return an access_token, but this access_token we can not use to query my application web services, to get my application access_token we need to pass username and password with grant_type as password. Below are my configuration files
AuthorizationServerConfiguration.java
#Configuration
#EnableAuthorizationServer
public class AuthorizationServerConfiguration extends AuthorizationServerConfigurerAdapter {
#Autowired
DataSource dataSource;
#Autowired
#Qualifier("authenticationManagerBean")
private AuthenticationManager authenticationManager;
#Override
public void configure(
AuthorizationServerSecurityConfigurer oauthServer)
throws Exception {
oauthServer.allowFormAuthenticationForClients();
}
#Override
public void configure(ClientDetailsServiceConfigurer clients)
throws Exception {
clients.jdbc(dataSource);
}
#Bean
#Primary
public DefaultTokenServices tokenServices() {
DefaultTokenServices tokenServices = new DefaultTokenServices();
tokenServices.setSupportRefreshToken(true);
tokenServices.setTokenStore(tokenStore());
tokenServices.setAccessTokenValiditySeconds(86400000);
tokenServices.setRefreshTokenValiditySeconds(86400000);
return tokenServices;
}
#Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints)
throws Exception {
endpoints
.tokenServices(tokenServices())
.authenticationManager(authenticationManager);
}
#Bean
public TokenStore tokenStore() {
return new JdbcTokenStore(dataSource);
}
}
ResourceServerConfiguration.java
#Configuration
#EnableResourceServer
public class ResourceServerConfiguration extends ResourceServerConfigurerAdapter {
private String resourceId = "rest_api";
#Override
public void configure(ResourceServerSecurityConfigurer resources) {
// #formatter:off
resources.resourceId(resourceId);
// #formatter:on
}
#Override
public void configure(HttpSecurity http) throws Exception {
http.csrf().disable().authorizeRequests()
.antMatchers(HttpMethod.OPTIONS, "/oauth/token").permitAll()
.antMatchers(HttpMethod.GET, "/**/login").permitAll()
.antMatchers(HttpMethod.GET, "/**/callback").permitAll()
.anyRequest().authenticated()
.and()
.formLogin().permitAll();
}
}
and finally WebSecurityConfigurerAdapter.java
#Configuration
#EnableGlobalMethodSecurity(prePostEnabled = true)
#EnableWebSecurity
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
#Autowired
UserDetailsService userDetailsService;
#Override
protected void configure(AuthenticationManagerBuilder auth)
throws Exception {
auth.userDetailsService(userDetailsService);
}
#Override
#Bean
public AuthenticationManager authenticationManagerBean()
throws Exception {
return super.authenticationManagerBean();
}
#Override
public void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.authorizeRequests()
.antMatchers(HttpMethod.OPTIONS, "/oauth/token").permitAll()
.antMatchers(HttpMethod.GET, "/**/login").permitAll()
.antMatchers(HttpMethod.GET, "/**/callback").permitAll()
.anyRequest().authenticated()
.and()
.formLogin().permitAll();
}
}
Have read different posts in SO, but couldn't get any working example, please guide me on this. Thanks in advance.!
String redirectURL = messages.getProperty(Constant.REDIRECT_URI.getValue());
String clientSecret = messages.getProperty(Constant.CLIENT_SECRET.getValue());
HttpHeaders header = new HttpHeaders();
header.setContentType(org.springframework.http.MediaType.APPLICATION_FORM_URLENCODED);
String req = "client_id=myas&" + "client_secret=" + clientSecret + "&grant_type=authorization_code&"
+ "scope=user_profile&" + "code=" + loginReqeust.getCode() + "&redirect_uri="
+ loginReqeust.getRedirectURL();
HttpEntity<String> body = new HttpEntity<String>(req, header);
Map<Object, Object> mapRes = new LinkedHashMap<Object, Object>();
// call to get access token
mapRes = getEndpoint("https://auth.mygov.in/oauth2/token", null, body, null);
String accessToken = mapRes.get("access_token").toString();
// Call for getting User Profile
String userUrl = "https://auth.mygov.in/myasoauth2/user/profile";
HttpHeaders head = new HttpHeaders();
head.add("Authorization", "Bearer " + accessToken);
HttpEntity<String> ent = new HttpEntity<String>(head);
Map<Object, Object> mapResponse = new LinkedHashMap<Object, Object>();
mapResponse.put("userProfile", getEndpoint(userUrl, null, ent, null));
//In my case userKey represents the username basically the email of the user using which he/she logged into facebook/google
String userKey = (String) ((LinkedHashMap<Object, Object>) mapResponse.get("userProfile")).get("mail");
// Store the user profile in your database with basic info like username & an autogenerated password for the time being and other basic fields.
userService.save(userprofileInfo);
mapResponse.put("username", "retrieved from facebook/google user's profile");
mapResponse.put("password", "autogenerated by your application");
//send back this response (mapResponse) to your UI and then from there make a call by passing this username and pwd to retrieve the access_token from your own applicatioon.
I had a similar requirement to get an access token from facebook and generate own JWT token by validating the facebook token on the server side.
I modified the project mentioned here:
https://github.com/svlada/springboot-security-jwt
My customizations are as follows(I am assuming you already have a facebook access token):
LoginRequest.java
public class LoginRequest {
private String token;
#JsonCreator
public LoginRequest(#JsonProperty("token") String token) {
this.token = token;
}
public String getToken() {
return token;
}
public void setToken(String token) {
this.token = token;
}
}
AjaxLoginProcessingFilter.java
#Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
throws AuthenticationException, IOException, ServletException {
if (!HttpMethod.POST.name().equals(request.getMethod()) || !WebUtil.isAjax(request)) {
if(logger.isDebugEnabled()) {
logger.debug("Authentication method not supported. Request method: " + request.getMethod());
}
throw new AuthMethodNotSupportedException("Authentication method not supported");
}
LoginRequest loginRequest = objectMapper.readValue(request.getReader(), LoginRequest.class);
if (StringUtils.isBlank(loginRequest.getToken())) {
throw new AuthenticationServiceException("token not provided");
}
UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(loginRequest.getToken(), null);
return this.getAuthenticationManager().authenticate(token);
}
AjaxAuthenticationProvider.java
#Component
public class AjaxAuthenticationProvider implements AuthenticationProvider {
#Autowired private BCryptPasswordEncoder encoder;
#Autowired private DatabaseUserService userService;
#Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
Assert.notNull(authentication, "No authentication data provided");
String username = null;
try {
username = getUsername(authentication.getPrincipal());
} catch (UnsupportedOperationException e) {
} catch (IOException e) {
}
//You can either register this user by fetching additional data from facebook or reject it.
User user = userService.getByUsername(username).orElseThrow(() -> new UsernameNotFoundException("User not found"));
if (user.getRoles() == null) throw new InsufficientAuthenticationException("User has no roles assigned");
List<GrantedAuthority> authorities = user.getRoles().stream()
.map(authority -> new SimpleGrantedAuthority(authority.getRole().authority()))
.collect(Collectors.toList());
UserContext userContext = UserContext.create(user.getUsername(), authorities);
return new UsernamePasswordAuthenticationToken(userContext, null, userContext.getAuthorities());
}
private String getUsername(Object principal) throws UnsupportedOperationException, IOException {
HttpClient client = new DefaultHttpClient();
//I am just accessing the details. You can debug whether this token was granted against your app.
HttpGet get = new HttpGet("https://graph.facebook.com/me?access_token=" + principal.toString());
HttpResponse response = client.execute(get);
BufferedReader rd = new BufferedReader(new InputStreamReader(response.getEntity().getContent()));
StringBuffer result = new StringBuffer();
String line = "";
while ((line = rd.readLine()) != null) {
result.append(line);
}
JSONObject o = new JSONObject(result.toString());
//This is just for demo. You should use id or some other unique field.
String username = o.getString("first_name");
return username;
}
#Override
public boolean supports(Class<?> authentication) {
return (UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication));
}
}
Apart from this, I also had to add custom BeanPostProcessor to override the default behavior of UsernamePasswordAuthenticationFilter to accept only token as a field instead of a username and a password.
UserPassAuthFilterBeanPostProcessor.java
public class UserPassAuthFilterBeanPostProcessor implements BeanPostProcessor {
private String usernameParameter;
private String passwordParameter;
#Override
public final Object postProcessAfterInitialization(final Object bean,
final String beanName) {
return bean;
}
#Override
public final Object postProcessBeforeInitialization(final Object bean,
final String beanName) {
if (bean instanceof UsernamePasswordAuthenticationFilter) {
final UsernamePasswordAuthenticationFilter filter =
(UsernamePasswordAuthenticationFilter) bean;
filter.setUsernameParameter(getUsernameParameter());
filter.setPasswordParameter(getPasswordParameter());
}
return bean;
}
public final void setUsernameParameter(final String usernameParameter) {
this.usernameParameter = usernameParameter;
}
public final String getUsernameParameter() {
return usernameParameter;
}
public final void setPasswordParameter(final String passwordParameter) {
this.passwordParameter = passwordParameter;
}
public final String getPasswordParameter() {
return passwordParameter;
}
Configuration:
#Bean
public UserPassAuthFilterBeanPostProcessor userPassAuthFilterBeanPostProcessor(){
UserPassAuthFilterBeanPostProcessor bean = new UserPassAuthFilterBeanPostProcessor();
bean.setUsernameParameter("token");
bean.setPasswordParameter(null);
return bean;
}

Resources