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

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(){
...
}

Related

Method security ignored when using Jersey with Spring Security

I'm having trouble getting Spring-Security to work with Jersey within my Spring Boot project. It seems to be something to do with the way the two are integrated but I can't figure out why...
In Jersey endpoints the Spring Security enforced authentication requirement (configured in the WebSecurityConfigurerAdapter) works fine however some method security annotations are ignored. JSR-250 annotations (such as #DenyAll, #RolesAllowed, etc) seem to work if one registers the RolesAllowedDynamicFeature feature. However, #PreAuthorize, #PostAuthorize, #PreFilter, #PostFilter don't work.
Also, when logging in via the /login endpoint the XSRF-TOKEN cookie is not sent. Normally Spring security handles this and if I rewrite the endpoint with Spring MVC, I get both the XSRF-TOKEN and my own ACCESS-TOKEN (generated and sent by the endpoint itself).
(I found a workaround to this - see end of post)
Here's some code (I left out the obvious stuff like JwtUtils, general bean config, etc). Full source (not that there's much - this is a pared-down version of the real project) can be found at https://github.com/ChambreNoire/jersey-issue.
Thanks!
(I'm using Spring Boot 2.3.10.REALEASE and upgrading isn't currently an option)
application.yaml
spring:
jersey:
application-path: /resources
servlet:
load-on-startup: 1
type: filter
JerseyConfig
#Configuration
public class JerseyConfig {
#Bean
public ResourceConfig resourceConfig(ObjectMapper objectMapper) {
return new ResourceConfig()
.property(ServletProperties.FILTER_FORWARD_ON_404, true)
.register((ContextResolver<ObjectMapper>) aClass -> objectMapper)
.register(JacksonFeature.class)
.register(HttpMethodOverrideFilter.class)
.register(AuthResource.class)
.register(UserResource.class);
}
}
SecurityConfigurationAdapter
#EnableWebSecurity
#EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true, jsr250Enabled = true)
public class SecurityConfigurationAdapter extends WebSecurityConfigurerAdapter {
private static final String[] PUBLIC_PATTERNS;
private final JwtTokenAuthFilter jwtTokenAuthFilter;
private final JwtTokenAuthEntryPoint unauthorizedHandler;
#Autowired
public SecurityConfigurationAdapter(final JwtTokenAuthFilter jwtTokenAuthFilter, final JwtTokenAuthEntryPoint unauthorizedHandler) {
this.jwtTokenAuthFilter = jwtTokenAuthFilter;
this.unauthorizedHandler = unauthorizedHandler;
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http.cors()
.configurationSource(corsConfig()).and()
.csrf()
.ignoringAntMatchers("/login", "/login2")
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()).and()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
.exceptionHandling()
.authenticationEntryPoint(unauthorizedHandler).and()
.authorizeRequests()
.antMatchers("/login", "/login2").permitAll()
.anyRequest().authenticated().and()
.addFilterBefore(jwtTokenAuthFilter, UsernamePasswordAuthenticationFilter.class)
.headers()
.xssProtection().and()
.contentSecurityPolicy("script-src 'self';require-trusted-types-for 'script';object-src 'none';");
}
private CorsConfigurationSource corsConfig() {
return request -> {
CorsConfiguration config = new CorsConfiguration();
config.setAllowedOrigins(singletonList("http://localhost:9000"));
config.setAllowedMethods(singletonList("*"));
config.setAllowCredentials(true);
config.setAllowedHeaders(singletonList("*"));
config.setExposedHeaders(singletonList("Authorization"));
config.setMaxAge(3600L);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", config);
return config;
};
}
}
JwtTokenAuthFilter
#Priority(Priorities.AUTHENTICATION)
public class JwtTokenAuthFilter extends OncePerRequestFilter {
private final UserDetailsService userDetailsService;
private final JwtTokenUtils jwtTokenUtils;
public JwtTokenAuthFilter(final UserDetailsService userDetailsService, final JwtTokenUtils jwtTokenUtils) {
this.userDetailsService = userDetailsService;
this.jwtTokenUtils = jwtTokenUtils;
}
#Override
protected void doFilterInternal(final HttpServletRequest req, final HttpServletResponse resp, final FilterChain chain)
throws ServletException, IOException {
if (req.getCookies() == null) {
chain.doFilter(req, resp);
return;
}
String token = Arrays.stream(req.getCookies())
.filter(c -> "ACCESS-TOKEN".equals(c.getName()))
.findFirst()
.map(Cookie::getValue)
.orElse(null);
if (isEmpty(token) || !jwtTokenUtils.validateJwtToken(token)) {
chain.doFilter(req, resp);
return;
}
String username = jwtTokenUtils.getUserNameFromJwtToken(token);
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(
userDetails, null, userDetails.getAuthorities());
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(req));
SecurityContextHolder.getContext().setAuthentication(authentication);
chain.doFilter(req, resp);
}
}
AuthResource
#Singleton
#Path("")
#Produces(MediaType.APPLICATION_JSON)
#Consumes(MediaType.APPLICATION_JSON)
public class AuthResource {
private final AuthenticationManager authenticationManager;
private final JwtTokenUtils jwtTokenUtils;
#Inject
public AuthResource(AuthenticationManager authenticationManager, JwtTokenUtils jwtTokenUtils) {
this.authenticationManager = authenticationManager;
this.jwtTokenUtils = jwtTokenUtils;
}
#POST
#Path("/login")
public Response login(#RequestBody LoginRequest request) {
try {
org.springframework.security.core.Authentication authentication = authenticationManager
.authenticate(
new UsernamePasswordAuthenticationToken(
request.getEmail(), request.getPassword()
)
);
UserDetailsImpl user = (UserDetailsImpl) authentication.getPrincipal();
return Response.ok()
.cookie(new NewCookie(
"ACCESS-TOKEN",
jwtTokenUtils.generateJwtToken(user),
"/", null, null,
NewCookie.DEFAULT_MAX_AGE,
true, true
))
.build();
} catch (BadCredentialsException ex) {
return Response.status(Response.Status.UNAUTHORIZED).build();
}
}
}
If anyone could enlighten me on why this is the case that would be most appreciated, or indeed any general comments/improvements you may have !
Cheers
UPDATE
It looks like adding the JWT cookie to the javax.ws.rs.core.Response is causing the issue although I'm not certain how. If I add it directly to the HttpServletResponse, the XSRF-TOKEN cookie is present in the response.
Here's the updated Jersey authentication endpoint :
#POST
#Path("/login")
public Response authenticateUser(#Context HttpServletResponse response, #RequestBody LoginRequest request) {
try {
Authentication authentication = authenticationManager
.authenticate(
new UsernamePasswordAuthenticationToken(
request.getEmail(), request.getPassword()
)
);
UserDetailsImpl user = (UserDetailsImpl) authentication.getPrincipal();
Cookie cookie = new Cookie("ACCESS-TOKEN", jwtTokenUtils.generateJwtToken(user));
cookie.setPath("/");
cookie.setSecure(true);
cookie.setHttpOnly(true);
cookie.setMaxAge(-1);
response.addCookie(cookie);
return Response.ok().build();
} catch (BadCredentialsException ex) {
return Response.status(Response.Status.UNAUTHORIZED).build();
}
}

How to log http request/response body even if OAuth2 validation failed?

I have a requirement to index(log) every request and response body of a Spring Boot MVC application into Elasticsearch application. I implemented the logging in a filter making it the highest priority(of order 1).
My application is acting as an OAuth2 resource server where it validates token from Auth Server. The problem is if the token validation failed, the request doesn't enter the filter, thus skipping the request and response log indexing part. If the token validation failed, the current logic throw the exception in the part:
public class MyJwtAccessTokenConverter extends JwtAccessTokenConverter {
#Override
protected Map<String, Object> decode(String token) {
//logic to throw token error
}
}
I want to index the request and response body even if the token is not validated. How can I do it? Is there any way to put Filter before token validation or any other specific way to log requests and responses?
Edit:
My ResourceServer config is as follows:
#Configuration
#EnableResourceServer
#EnableGlobalMethodSecurity(prePostEnabled = true)
#Log4j2
public class ResourceServerConfiguration extends ResourceServerConfigurerAdapter {
#Value("${spring.application.name}")
public String applicationResourceID;
#Value(" ${key.config.oauth2.publicKey}")
private String publicKey;
#Value("jwt.aes.encrypt.keyValue")
String jwtAesEncryptionKey;
#Override
public void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers(
"test/**"
).permitAll()
.and().authorizeRequests().anyRequest().authenticated().and()
.cors().and()
.csrf().disable()
.httpBasic().disable()
.exceptionHandling()
.authenticationEntryPoint(
(request, response, authException) -> response.sendError(HttpServletResponse.SC_UNAUTHORIZED)
)
.accessDeniedHandler(
(request, response, authException) -> response.sendError(HttpServletResponse.SC_UNAUTHORIZED)
);
}
#Override
public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
OAuth2AuthenticationEntryPoint authenticationEntryPoint = new OAuth2AuthenticationEntryPoint();
authenticationEntryPoint.setExceptionTranslator(oauth2ResponseExceptionTranslator());
resources.authenticationEntryPoint(authenticationEntryPoint);
OAuth2AccessDeniedHandler accessDeniedHandler = new OAuth2AccessDeniedHandler();
accessDeniedHandler.setExceptionTranslator(oauth2ResponseExceptionTranslator());
resources.accessDeniedHandler(accessDeniedHandler);
resources.authenticationEntryPoint(authenticationEntryPoint)
.accessDeniedHandler(accessDeniedHandler)
.resourceId(applicationResourceID)
.tokenStore(tokenStore());
}
#Bean
public TokenStore tokenStore() {
return new JwtTokenStore(jwtAccessTokenConverter());
}
#Bean
public JwtAccessTokenConverter jwtAccessTokenConverter() {
MyJwtAccessTokenConverter converter = new MyJwtAccessTokenConverter(jwtAesEncryptionKey);
converter.setVerifierKey(publicKey);
return converter;
}
#Bean
#Profile(value = {"local"})
CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedOrigins(Arrays.asList("http://localhost:4200"));
configuration.setAllowedHeaders(Arrays.asList("authorization", "content-type"));
configuration.setAllowedMethods(Arrays.asList("GET", "POST"));
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
return source;
}
#Bean
public WebResponseExceptionTranslator oauth2ResponseExceptionTranslator() {
return new DefaultWebResponseExceptionTranslator() {
#Override
public ResponseEntity<OAuth2Exception> translate(Exception e) throws Exception {
try {
log.info("Oauth2ExceptionTranslatorConfiguration", e);
ResponseEntity<OAuth2Exception> responseEntity = super.translate(e);
OAuth2Exception oAuth2ExceptionBody = responseEntity.getBody();
HttpStatus statusCode = responseEntity.getStatusCode();
OAuth2Exception myOAuth2Response = OAuth2Exception.create(oAuth2ExceptionBody.getOAuth2ErrorCode(), getMessage(oAuth2ExceptionBody));
myOAuth2Response.addAdditionalInformation("message", myOAuth2Response.getMessage());
myOAuth2Response.addAdditionalInformation("isSuccess", "false");
HttpHeaders headers = new HttpHeaders();
headers.setAll(responseEntity.getHeaders().toSingleValueMap());
return new ResponseEntity<>(myOAuth2Response, headers, statusCode);
} catch (Exception ex) {
log.info("Oauth2ExceptionTranslatorConfiguration", ex);
return new ResponseEntity<>(new OAuth2Exception("Error"), null, HttpStatus.INTERNAL_SERVER_ERROR);
}
}
};
}
private String getMessage(OAuth2Exception oAuth2Exception) {
if (oAuth2Exception instanceof InvalidTokenException) {
return "Invalid Token";
} else if (oAuth2Exception instanceof InvalidGrantException) {
return "Invalid Username or password";
} else if (oAuth2Exception instanceof InvalidRequestException) {
return "Invalid Request";
}
return oAuth2Exception.getOAuth2ErrorCode();
}
}
The idea is to use custom exception and ControllerAdvice for this purpose
So my example code like
public class CustomJwtAccessTokenConverter extends JwtAccessTokenConverter {
#Override
protected Map<String, Object> decode(String token) {
Map<String, Object> pieces = null;
try {
//
} catch(InvalidTokenException ex) {
throw new InvalidTokenException("MY CUSTOM MESSAGE");
}
//
}
}
And you can alse use ControllerAdvice to catch what you want
#ControllerAdvice
public class MyExceptionHandler extends ResponseEntityExceptionHandler {
#ResponseStatus(value = HttpStatus.BAD_REQUEST)
#ExceptionHandler({HttpMessageNotReadableException.class, MethodArgumentNotValidException.class,
HttpRequestMethodNotSupportedException.class})
public ResponseEntity<Object> InvalidTokenException(HttpServletRequest req, Exception exception) {
// log you request and your response
}
}
add HttpServletResponse response if you want response
Hope useful
I found a simple solution for it. If I put my filter order as HIGHEST_PRECEDENCE with annotation #Order(Ordered.HIGHEST_PRECEDENCE), then it enters before token validation(where I can log request) and exit after all filters/operations where I can log response as well.

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

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.

Extract Currently Logged in User information from JWT token using Spring Security

I have implemented JWT and LDAP Authentication using Spring Security Oauth2. It seems to be working fine and I can login with my LDAP credentials.
Now, there is one requirement that I need to use the currently logged in user info to save details in database - specifically like when that user add/update a new record. I tried to get that using Spring security way using
SecurityContextHolder.getContext().getAuthentication().getDetails()
but it doesn't return all that information which I have in JWT. It just returns Remote IP,the JWT token value and authenticated true. It doesn't even return name().
I am new to JWT, so not sure if I need to extract it by reading that token and even how we can achieve it.
Any pointers will be appreciated.
Thanks.
The first thing you need to do is store the user information inside the JWT when it is created, then you have to extract it when it is used. I had a similar situation and I solved it by extending both the TokenEnhancer and JwtAccessTokenConverter.
I use the TokenEnhancer to embed my extended principal of type CustomUserDetailsinside the JWT additional information.
public class CustomAccessTokenEnhancer implements TokenEnhancer {
#Override
public OAuth2AccessToken enhance(OAuth2AccessToken accessToken, OAuth2Authentication authentication) {
Authentication userAuthentication = authentication.getUserAuthentication();
if (userAuthentication != null) {
Object principal = authentication.getUserAuthentication().getPrincipal();
if (principal instanceof CustomUserDetails) {
Map<String, Object> additionalInfo = new HashMap<>();
additionalInfo.put("userDetails", principal);
((DefaultOAuth2AccessToken) accessToken).setAdditionalInformation(additionalInfo);
}
}
return accessToken;
}
}
And then manually extract the extended principal when building the Authentication object when processing an authenticated request.
public class CustomJwtAccessTokenConverter extends JwtAccessTokenConverter {
#Override
public OAuth2Authentication extractAuthentication(Map<String, ?> map) {
OAuth2Authentication authentication = super.extractAuthentication(map);
Authentication userAuthentication = authentication.getUserAuthentication();
if (userAuthentication != null) {
LinkedHashMap userDetails = (LinkedHashMap) map.get("userDetails");
if (userDetails != null) {
// build your principal here
String localUserTableField = (String) userDetails.get("localUserTableField");
CustomUserDetails extendedPrincipal = new CustomUserDetails(localUserTableField);
Collection<? extends GrantedAuthority> authorities = userAuthentication.getAuthorities();
userAuthentication = new UsernamePasswordAuthenticationToken(extendedPrincipal,
userAuthentication.getCredentials(), authorities);
}
}
return new OAuth2Authentication(authentication.getOAuth2Request(), userAuthentication);
}
}
and the AuthorizationServer configuration to tie it all together.
#Configuration
#EnableAuthorizationServer
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
#Autowired
private AuthenticationManager authenticationManager;
#Autowired
private UserDetailsService userDetailsService;
#Autowired
private DataSource dataSource;
#Bean
public JwtAccessTokenConverter accessTokenConverter() {
CustomJwtAccessTokenConverter accessTokenConverter = new CustomJwtAccessTokenConverter();
accessTokenConverter.setSigningKey("a1b2c3d4e5f6g");
return accessTokenConverter;
}
#Bean
public TokenStore tokenStore() {
return new JwtTokenStore(accessTokenConverter());
}
#Bean
#Primary
public DefaultTokenServices tokenServices() {
DefaultTokenServices defaultTokenServices = new DefaultTokenServices();
defaultTokenServices.setTokenStore(tokenStore());
defaultTokenServices.setSupportRefreshToken(true);
return defaultTokenServices;
}
#Bean
public TokenEnhancer tokenEnhancer() {
return new CustomAccessTokenEnhancer();
}
#Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
#Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.jdbc(dataSource).passwordEncoder(passwordEncoder());
}
#Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain();
tokenEnhancerChain.setTokenEnhancers(Arrays.asList(tokenEnhancer(), accessTokenConverter()));
endpoints
.tokenStore(tokenStore())
.tokenEnhancer(tokenEnhancerChain)
.authenticationManager(authenticationManager)
.userDetailsService(userDetailsService);
}
#Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
security.passwordEncoder(passwordEncoder());
security.checkTokenAccess("isAuthenticated()");
}
}
I am then able to access my extended principal in my resource controller like this
#RestController
public class SomeResourceController {
#RequestMapping("/some-resource")
public ResponseEntity<?> someResource(Authentication authentication) {
CustomUserDetails userDetails = (CustomUserDetails) authentication.getPrincipal();
return ResponseEntity.ok("woo hoo!");
}
}
Hope this helps!
In your REST Service, add the OAuth2Authentication Class as an argument
#RequestMapping(value = "/{id}/products", method = RequestMethod.POST)
public ResourceResponse<String> processProducts(OAuth2Authentication auth) {
Springboot will automatically map the logged-in user details to this object. Now, you can do the following to access the username
auth.getPrincipal().toString()

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