I have a project for which I had a custom implementation of a JWT token and I could get an access token from an integration test with the call to the method:
private void addTokenToRequestHeader(HttpHeaders headers, String username) {
tokenAuthenticationService.addAccessTokenToHeader(headers, username);
}
Now I'm changing the security to use OAuth2 and my configuration is not anymore using the custom JWT implementation:
#Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.inMemory()
// The client id and client secret
.withClient(OAUTH_CLIENT_ID)
.secret(OAUTH_CLIENT_SECRET)
// The endpoint at the client application to redirect to
.redirectUris(OAUTH_CLIENT_URL)
// The type of request the authorization server expects for the client
.authorizedGrantTypes(OAUTH_GRANT_TYPE_PASSWORD, OAUTH_GRANT_TYPE_AUTHORIZATION_CODE, OAUTH_GRANT_TYPE_REFRESH_TOKEN)
// The permissions the client needs to send requests to the authorization server
.authorities("ROLE_CLIENT", "ROLE_TRUSTED_CLIENT")
// The resources server id
.resourceIds(RESOURCE_SERVER_ID)
// The scope of content offered by the resources servers
.scopes("read_profile", "write_profile", "read_firstname")
// The lifespan of the tokens for the client application
.accessTokenValiditySeconds(jwtProperties.getAccessTokenExpirationTime())
.refreshTokenValiditySeconds(jwtProperties.getRefreshTokenExpirationTime());
}
#Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
security
.tokenKeyAccess("permitAll()")
.checkTokenAccess("isAuthenticated()")
.passwordEncoder(oauthClientPasswordEncoder);
}
#Autowired
#Qualifier("authenticationManagerBean")
private AuthenticationManager authenticationManager;
#Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints
.authenticationManager(authenticationManager)
.tokenServices(defaultTokenServices())
.allowedTokenEndpointRequestMethods(HttpMethod.GET, HttpMethod.POST)
.tokenEnhancer(jwtAccessTokenConverter())
.accessTokenConverter(jwtAccessTokenConverter())
.userDetailsService(userDetailsService);
}
#Bean
public JwtAccessTokenConverter jwtAccessTokenConverter() {
JwtAccessTokenConverter jwtAccessTokenConverter = new JwtAccessTokenConverter();
jwtAccessTokenConverter.setKeyPair(new KeyStoreKeyFactory(new ClassPathResource(jwtProperties.getSslKeystoreFilename()), jwtProperties.getSslKeystorePassword().toCharArray()).getKeyPair(jwtProperties.getSslKeyPair()));
return jwtAccessTokenConverter;
}
#Bean
public TokenEnhancer tokenEnhancer() {
return new CustomTokenEnhancer();
}
// Add user information to the token
class CustomTokenEnhancer implements TokenEnhancer {
#Override
public OAuth2AccessToken enhance(OAuth2AccessToken accessToken, OAuth2Authentication authentication) {
User user = (User) authentication.getPrincipal();
Map<String, Object> info = new LinkedHashMap<String, Object>(accessToken.getAdditionalInformation());
info.put(CommonConstants.JWT_CLAIM_USER_EMAIL, user.getEmail().getEmailAddress());
info.put(CommonConstants.JWT_CLAIM_USER_FULLNAME, user.getFirstname() + " " + user.getLastname());
info.put("scopes", authentication.getAuthorities().stream().map(s -> s.toString()).collect(Collectors.toList()));
info.put("organization", authentication.getName());
DefaultOAuth2AccessToken customAccessToken = new DefaultOAuth2AccessToken(accessToken);
customAccessToken.setAdditionalInformation(info);
customAccessToken.setExpiration(tokenAuthenticationService.getExpirationDate());
return customAccessToken;
}
}
#Bean
public TokenStore tokenStore() {
return new JwtTokenStore(jwtAccessTokenConverter());
}
#Bean
#Primary
public DefaultTokenServices defaultTokenServices() {
DefaultTokenServices defaultTokenServices = new DefaultTokenServices();
defaultTokenServices.setTokenStore(tokenStore());
defaultTokenServices.setSupportRefreshToken(true);
return defaultTokenServices;
}
And its accompanying security configuration:
#Bean
public UserDetailsService userDetailsService() {
return new UserDetailsServiceImpl();
}
#Bean
#Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
#Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService).passwordEncoder(userPasswordEncoder);
}
// Allow preflight requests
#Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers(HttpMethod.OPTIONS, "/**");
}
Now, to get the access token, I have to send a request:
#Before
public void setup() throws Exception {
super.setup();
addTokenToRequestHeader(httpHeaders, ClientFixtureService.CLIENT_ID, UserFixtureService.USER_EMAIL, UserFixtureService.USER_PASSWORD);
}
private void addTokenToRequestHeader(HttpHeaders headers, String oauthClientId, String username, String password) throws Exception {
String token = getOAuthAccessToken(oauthClientId, username, password);
headers.remove(CommonConstants.ACCESS_TOKEN_HEADER_NAME);
headers.add(CommonConstants.ACCESS_TOKEN_HEADER_NAME, tokenAuthenticationService.buildOAuthAccessToken(token));
}
private void addBase64UserPasswordHeaders(String username, String password, HttpHeaders httpHeaders) {
httpHeaders.setContentType(MediaType.APPLICATION_JSON);
httpHeaders.setAccept(Arrays.asList(MediaType.APPLICATION_JSON));
String usernamePassword = username + ":" + password;
String encodedAuthorisation = Base64.getEncoder().encodeToString(usernamePassword.getBytes(UTF_8));
httpHeaders.add(CommonConstants.ACCESS_TOKEN_HEADER_NAME,
CommonConstants.AUTH_BASIC + " " + new String(encodedAuthorisation));
}
private String getOAuthAccessToken(String oauthClientId, String username, String password) throws Exception {
MultiValueMap<String, String> oauthParams = new LinkedMultiValueMap<>();
oauthParams.add("grant_type", AuthorizationServerConfiguration.OAUTH_GRANT_TYPE_PASSWORD);
oauthParams.add("client_id", oauthClientId);
oauthParams.add("username", username);
oauthParams.add("password", password);
addBase64UserPasswordHeaders(AuthorizationServerConfiguration.OAUTH_CLIENT_ID, AuthorizationServerConfiguration.OAUTH_CLIENT_SECRET, httpHeaders);
ResultActions mvcResult = this.mockMvc
.perform(post(RESTConstants.SLASH + DomainConstants.AUTH + RESTConstants.SLASH + DomainConstants.TOKEN)
.headers(httpHeaders)
.params(oauthParams)
.contentType(MediaType.APPLICATION_JSON).accept(MediaType.APPLICATION_JSON))
.andDo(print())
.andExpect(status().isOk());
String resultString = mvcResult.andReturn().getResponse().getContentAsString();
JacksonJsonParser jsonParser = new JacksonJsonParser();
return jsonParser.parseMap(resultString).get("access_token").toString();
}
But when running the debugger, my user details implementation loadUserByUsername method is never called up. It was called up before when I had a custom implementation of a JWT token and no OAuth2 configuration:
#Service
public class UserDetailsServiceImpl implements UserDetailsService {
#Autowired
private CredentialsService credentialsService;
#Override
#Transactional
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
if (username != null && !username.isEmpty()) {
try {
User user = credentialsService.findByEmail(new EmailAddress(username));
return new UserDetailsWrapper(user);
} catch (EntityNotFoundException e) {
throw new UsernameNotFoundException("The user " + username + " was not found.");
}
}
throw new UsernameNotFoundException("The user " + username + " was not found.");
}
}
My request looks like:
MockHttpServletRequest:
HTTP Method = POST
Request URI = /auth/token
Parameters = {grant_type=[password], client_id=[ng-xxx], username=[xxx#yahoo.xx], password=[xxxx]}
Headers = {Content-Type=[application/json, application/json], Accept=[application/json], Authorization=[Basic bmctemxxbzpzZWNyZXQ=]}
Body = <no character encoding set>
Here is what the console log has to say:
2019-01-08 09:08:11.841 DEBUG 18338 --- [ main] o.s.s.w.a.www.BasicAuthenticationFilter : Authentication request for failed: org.springframework.security.authentication.BadCredentialsException: Bad credentials
2019-01-08 09:08:11.842 DEBUG 18338 --- [ main] s.w.a.DelegatingAuthenticationEntryPoint : Trying to match using RequestHeaderRequestMatcher [expectedHeaderName=X-Requested-With, expectedHeaderValue=XMLHttpRequest]
2019-01-08 09:08:11.842 DEBUG 18338 --- [ main] s.w.a.DelegatingAuthenticationEntryPoint : No match found. Using default entry point org.springframework.security.web.authentication.www.BasicAuthenticationEntryPoint#6cb84986
You get the following Exception when you try to POST to /oauth/token with Authorization header with Basic credentials. This with the fact that your UserDetailsService.loadUserByUsername method never gets called means that your OAuth2 Client Credentials ng-zlqo:secret are not registered in the OAuth2 AuthorizationServer.
o.s.s.w.a.www.BasicAuthenticationFilter : Authentication request for failed: org.springframework.security.authentication.BadCredentialsException: Bad credentials
Related
We have login page where user will enter user credentials and internally calling one more authenticate service where need to store this token and pass to all REST controllers.I tried configuring bean scope within this class and but getting below exception .we are using spring 5.x;
com.config.CustomAuthenticationProvider sessionScopedBean
CustomAuthenticationProvider UserDetails !!!null
Jun 20, 2020 11:52:37 AM org.apache.catalina.core.StandardWrapperValve invoke
java.lang.ClassCastException: org.springframework.beans.factory.support.NullBean cannot be cast to com.utils.UserDetails
#Component
public class CustomAuthenticationProvider implements AuthenticationProvider {
private Logger logger = Logger.getLogger(getClass().getName());
private UserDetails userDetails;
#Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
String userName = authentication.getName();
String passWord = authentication.getCredentials().toString();
Result response;
try {
response = CustomClient.authenticate(userName, passWord);
} catch (Exception e) {
throw new BadCredentialsException("system authentication failed");
}
if (response != null && response.getToken() != null) {
//need to store this response.getToken() in session
logger.info("Token: " + response.getToken());
userDetails= new UserDetails();
userDetails.setToken(response.getToken());
logger.info("Authentication SUCCESS !!!");
return new UsernamePasswordAuthenticationToken(userName, passWord, Collections.emptyList());
} else {
logger.info("Authentication FAILED...");
throw new BadCredentialsException("authentication failed");
}
}
#Bean
#Scope(value = WebApplicationContext.SCOPE_SESSION, proxyMode = ScopedProxyMode.TARGET_CLASS)
public UserDetails sessionScopedBean() {
logger.info(" UserDetails !!!"+userDetails);
return userDetails;
}
#Override
public boolean supports(Class<?> auth) {
return auth.equals(UsernamePasswordAuthenticationToken.class);
}
}
Why do you want to create session scoped UserDetails bean in the first place? You can already achieve it by doing the following:
#GetMapping("/abc")
public void getUserProfile(#AuthenticationPrincipal UserDetails user ) {
...
}
or
#GetMapping("/abc")
public void getUserProfile() {
SecurityContext securityContext = SecurityContextHolder.getContext();
UserDetails user = (UserDetails) securityContext.getAuthentication().getPrincipal();
}
Note:
Behind the scene, spring uses HttpSessionSecurityContextRepository to store your SecurityContext in http session and restore it back on every request
And the Updated CustomAuthenticationProvider
#Component
public class CustomAuthenticationProvider implements AuthenticationProvider {
private Logger logger = Logger.getLogger(getClass().getName());
#Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
String userName = authentication.getName();
String passWord = authentication.getCredentials().toString();
Result response;
try {
response = CustomClient.authenticate(userName, passWord);
} catch (Exception e) {
throw new BadCredentialsException("system authentication failed");
}
if (response != null && response.getToken() != null) {
//need to store this response.getToken() in session
logger.info("Token: " + response.getToken());
UserDetails userDetails= new UserDetails();
userDetails.setToken(response.getToken());
logger.info("Authentication SUCCESS !!!");
return new UsernamePasswordAuthenticationToken(userDetails, passWord, Collections.emptyList());
} else {
logger.info("Authentication FAILED...");
throw new BadCredentialsException("authentication failed");
}
}
#Override
public boolean supports(Class<?> auth) {
return auth.equals(UsernamePasswordAuthenticationToken.class);
}
}
First of all you can not create Bean like your example. #Bean annotation is processed when application context is starting. UserDetails will be null so it can not be created.
You are creating UserDetails after applacation context is up.
Do you really want to keep session if that's case
#Configuration
public class Config {
#Bean
#Scope(value = WebApplicationContext.SCOPE_SESSION, proxyMode = ScopedProxyMode.TARGET_CLASS)
public UserDetails userDetails() {
return new UserDetails();
}
}
#Component
public class CustomAuthenticationProvider implements AuthenticationProvider {
private Logger logger = Logger.getLogger(getClass().getName());
#Autowired
private UserDetails userDetails;
}
You can inject by Autowire or constructor injection
Don't instantiate it manually just inject it and use in method like below
userDetails.setToken(response.getToken());
I have a spring boot application which generates a jwt token on successful login. and the token is returned to the user, but on each restart of the application the token is invalidated. I have a secret key stored on the properties file for now to test. here is my code to generate it,
public String createToken(String username, String role) {
Claims claims = Jwts.claims().setSubject(username);
claims.put("auth", role);
claims.put("test", "test");
Date now = new Date();
Date validity = new Date(now.getTime() + validityInMilliseconds);
return Jwts.builder()//
.setClaims(claims)//
.setIssuedAt(now)//
.setExpiration(validity)//
.signWith(SignatureAlgorithm.HS256, secretKey)//
.compact();
}
I would like to have the token validated even though the application is restarted. Any suggestions on what I could be doing wrong is appreciated.
My Config class
public class JwtTokenFilterConfigurer extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> {
private JwtTokenProvider jwtTokenProvider;
public JwtTokenFilterConfigurer(JwtTokenProvider jwtTokenProvider) {
this.jwtTokenProvider = jwtTokenProvider;
}
#Override
public void configure(HttpSecurity http) throws Exception {
JwtTokenFilter customFilter = new JwtTokenFilter(jwtTokenProvider);
http.addFilterBefore(customFilter, UsernamePasswordAuthenticationFilter.class);
}
}
Preface
I am working on an OAuth application for security between two servers. I have an OAuth Server and a Resource Server. The Resource Server has a single .war deployed that contains 4 APIs.
Single Responsibility
The OAuth server has to validate a the access token that was passed by an API (1 of the 4) from that same .war.
The OAuth server has to keep a hit count for a particular accessToken for a particular API. If the hit count exceeds the configured hits the OAuth server would throw a 403: Forbidden.
Every API in the .war must first validate the accessToken from the OAuth server and if it's validated, then proceed to provide the response.
What I've done:
If a .war has a single API then I can simply make the two servers communicate using a webHook, below is the code that does it.
On the Resource Server Side:
My urls for different APIs are:
localhost:8080/API/API1
localhost:8080/API/API2
Below code routes any request if they have /API/anything towards the spring security filters
<http pattern="/API/**" create-session="never" authentication-manager-ref="authenticationManager" entry-point-ref="oauthAuthenticationEntryPoint" xmlns="http://www.springframework.org/schema/security">
<anonymous enabled="false" />
<intercept-url pattern="/places/**" method="GET" access="IS_AUTHENTICATED_FULLY" />
<custom-filter ref="resourceServerFilter" before="PRE_AUTH_FILTER" />
<access-denied-handler ref="oauthAccessDeniedHandler" />
</http>
I have used remote token services and defined the webHook to route the request to the OAuth server
<bean id="tokenServices" class="org.springframework.security.oauth2.provider.token.RemoteTokenServices">
<property name="checkTokenEndpointUrl" value="http://localhost:8181/OUTPOST/oauth/check_token"/>
<property name="clientId" value="atlas"/>
<property name="clientSecret" value="atlas"/>
</bean>
Configuration for Auth server
#EnableAuthorizationServer
#Configuration
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
private static String REALM="OUTPOST_API";
#Autowired
private ClientDetailsService clientService;
#Autowired
public AuthorizationServerConfig(AuthenticationManager authenticationManager,RedisConnectionFactory redisConnectionFactory) {
this.authenticationManager = authenticationManager;
this.redisTokenStore = new RedisTokenStore(redisConnectionFactory);
}
// #Autowired
// #Qualifier("authenticationManagerBean")
private AuthenticationManager authenticationManager;
private TokenStore redisTokenStore;
#Autowired
private UserApprovalHandler userApprovalHandler;
#Autowired
private RedisConnectionFactory redisConnectionFactory;
#Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
security.tokenKeyAccess("isAuthenticated()")
.checkTokenAccess("isAuthenticated()").
realm(REALM+"/client");
}
#Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients
.inMemory()
.withClient("cl1")
.secret("pwd")
.authorizedGrantTypes("password", "client_credentials", "refresh_token")
.authorities("ROLE_CLIENT", "ROLE_ADMIN")
.scopes("read", "write", "trust")/*
.resourceIds("sample-oauth")*/
.accessTokenValiditySeconds(1000)
.refreshTokenValiditySeconds(5000)
.and()
.withClient("atlas")
.secret("atlas");
}
#Bean
#Autowired
public TokenStore tokenStore(RedisConnectionFactory redisConnectionFactory) {
this.redisTokenStore = new RedisTokenStore(redisConnectionFactory);
return this.redisTokenStore;
}
#Bean
public WebResponseExceptionTranslator loggingExceptionTranslator() {
return new DefaultWebResponseExceptionTranslator() {
#Override
public ResponseEntity<OAuth2Exception> translate(Exception e) throws Exception {
// This is the line that prints the stack trace to the log. You can customise this to format the trace etc if you like
e.printStackTrace();
// Carry on handling the exception
ResponseEntity<OAuth2Exception> responseEntity = super.translate(e);
HttpHeaders headers = new HttpHeaders();
headers.setAll(responseEntity.getHeaders().toSingleValueMap());
OAuth2Exception excBody = responseEntity.getBody();
return new ResponseEntity<>(excBody, headers, responseEntity.getStatusCode());
}
};
}
#Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints.tokenStore(redisTokenStore).userApprovalHandler(userApprovalHandler)
.authenticationManager(authenticationManager)
.exceptionTranslator(loggingExceptionTranslator());
}
public void setRedisConnectionFactory(RedisConnectionFactory redisConnectionFactory) {
this.redisConnectionFactory = redisConnectionFactory;
}
#Bean
public TokenStoreUserApprovalHandler userApprovalHandler(){
TokenStoreUserApprovalHandler handler = new TokenStoreUserApprovalHandler();
handler.setTokenStore(redisTokenStore);
handler.setRequestFactory(new DefaultOAuth2RequestFactory(clientService));
handler.setClientDetailsService(clientService);
return handler;
}
#Bean
#Autowired
public ApprovalStore approvalStore() throws Exception {
TokenApprovalStore store = new TokenApprovalStore();
store.setTokenStore(redisTokenStore);
return store;
}
#Bean
#Primary
#Autowired
public DefaultTokenServices tokenServices() {
DefaultTokenServices tokenServices = new DefaultTokenServices();
tokenServices.setSupportRefreshToken(true);
tokenServices.setTokenStore(redisTokenStore);
return tokenServices;
}
}
#Component
class MyOAuth2AuthenticationEntryPoint extends OAuth2AuthenticationEntryPoint{}
What I need help with:
The issue is with the support for single .war and multiple API. The issue is the spring config is created at a package level because of which all the APIs in the .war have the same clientID and clientSecret.
How would my OAuth server know, which specific API is being accessed and of which API the hitCount needs to be deducted.
Possible Solution?
I was thinks of customizing RemoteTokenService and adding a request parameter at the webHoot URL and then using a filter at OAuth server to get the passed tag (if I may call it that)
Is this even possible? Is there any better approch than this, that doesn't involve all these work arounds?
Eureka !! I finally found a way out to resolve this problem.
All you have to do is :
Configuration at Resource server
Instead of using RemoteTokenService make a custom remote token service which appends some data (query parameter) in the generated request.
public class CustomRemoteTokenService implements ResourceServerTokenServices {
protected final Log logger = LogFactory.getLog(getClass());
private RestOperations restTemplate;
private String checkTokenEndpointUrl;
private String clientId;
private String clientSecret;
private String tokenName = "token";
private AccessTokenConverter tokenConverter = new DefaultAccessTokenConverter();
#Autowired
public CustomRemoteTokenService() {
restTemplate = new RestTemplate();
((RestTemplate) restTemplate).setErrorHandler(new DefaultResponseErrorHandler() {
#Override
// Ignore 400
public void handleError(ClientHttpResponse response) throws IOException {
if (response.getRawStatusCode() != 400) {
super.handleError(response);
}
}
});
}
public void setRestTemplate(RestOperations restTemplate) {
this.restTemplate = restTemplate;
}
public void setCheckTokenEndpointUrl(String checkTokenEndpointUrl) {
this.checkTokenEndpointUrl = checkTokenEndpointUrl;
}
public void setClientId(String clientId) {
this.clientId = clientId;
}
public void setClientSecret(String clientSecret) {
this.clientSecret = clientSecret;
}
public void setAccessTokenConverter(AccessTokenConverter accessTokenConverter) {
this.tokenConverter = accessTokenConverter;
}
public void setTokenName(String tokenName) {
this.tokenName = tokenName;
}
#Override
public OAuth2Authentication loadAuthentication(String accessToken) throws AuthenticationException, InvalidTokenException {
/*
* This code needs to be more dynamic. Every time an API is added we have to add its entry in the if check for now.
* Should be changed later.
*/
HttpServletRequest request = Context.getCurrentInstance().getRequest();
MultiValueMap<String, String> formData = new LinkedMultiValueMap<String, String>();
String uri = request.getRequestURI();
formData.add(tokenName, accessToken);
if(request != null) {
if(uri.contains("API1")) {
formData.add("api", "1");
}else if(uri.contains("API2")) {
formData.add("api", "2");
}
}
HttpHeaders headers = new HttpHeaders();
headers.set("Authorization", getAuthorizationHeader(clientId, clientSecret));
Map<String, Object> map = postForMap(checkTokenEndpointUrl, formData, headers);
if (map.containsKey("error")) {
logger.debug("check_token returned error: " + map.get("error"));
throw new InvalidTokenException(accessToken);
}
Assert.state(map.containsKey("client_id"), "Client id must be present in response from auth server");
return tokenConverter.extractAuthentication(map);
}
#Override
public OAuth2AccessToken readAccessToken(String accessToken) {
throw new UnsupportedOperationException("Not supported: read access token");
}
private String getAuthorizationHeader(String clientId, String clientSecret) {
String creds = String.format("%s:%s", clientId, clientSecret);
try {
return "Basic " + new String(Base64.encode(creds.getBytes("UTF-8")));
}
catch (UnsupportedEncodingException e) {
throw new IllegalStateException("Could not convert String");
}
}
private Map<String, Object> postForMap(String path, MultiValueMap<String, String> formData, HttpHeaders headers) {
if (headers.getContentType() == null) {
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
}
#SuppressWarnings("rawtypes")
Map map = restTemplate.exchange(path, HttpMethod.POST,
new HttpEntity<MultiValueMap<String, String>>(formData, headers), Map.class).getBody();
#SuppressWarnings("unchecked")
Map<String, Object> result = map;
return result;
}
}
By implementing ResourceServerTokenServices you can modify the request that is sent by the resource server to the auth server for authentication and authorization.
configuration at Auth Server
Override the spring security controller. What i mean by overring is make a custom controller so that the request for oauth/check_token is handled by your custom controller and not the spring defined controller.
#RestController
public class CustomCheckTokenEndpoint {
private ResourceServerTokenServices resourceServerTokenServices;
private AccessTokenConverter accessTokenConverter = new DefaultAccessTokenConverter();
protected final Log logger = LogFactory.getLog(getClass());
private WebResponseExceptionTranslator exceptionTranslator = new DefaultWebResponseExceptionTranslator();
#Autowired
KeyHitManager keyHitManager;
public CustomCheckTokenEndpoint(ResourceServerTokenServices resourceServerTokenServices) {
this.resourceServerTokenServices = resourceServerTokenServices;
}
/**
* #param exceptionTranslator
* the exception translator to set
*/
public void setExceptionTranslator(WebResponseExceptionTranslator exceptionTranslator) {
this.exceptionTranslator = exceptionTranslator;
}
/**
* #param accessTokenConverter
* the accessTokenConverter to set
*/
public void setAccessTokenConverter(AccessTokenConverter accessTokenConverter) {
this.accessTokenConverter = accessTokenConverter;
}
#RequestMapping(value = "/oauth/check_token")
#ResponseBody
public Map<String, ?> customCheckToken(#RequestParam("token") String value, #RequestParam("api") int api) {
OAuth2AccessToken token = resourceServerTokenServices.readAccessToken(value);
if (token == null) {
throw new InvalidTokenException("Token was not recognised");
}
if (token.isExpired()) {
throw new InvalidTokenException("Token has expired");
}
OAuth2Authentication authentication = resourceServerTokenServices.loadAuthentication(token.getValue());
Map<String, ?> response = accessTokenConverter.convertAccessToken(token, authentication);
String clientId = (String) response.get("client_id");
if (!keyHitManager.isHitAvailble(api,clientId)) {
throw new InvalidTokenException(
"Services for this key has been suspended due to daily/hourly transactions limit");
}
return response;
}
#ExceptionHandler(InvalidTokenException.class)
public ResponseEntity<OAuth2Exception> handleException(Exception e) throws Exception {
logger.info("Handling error: " + e.getClass().getSimpleName() + ", " + e.getMessage());
// This isn't an oauth resource, so we don't want to send an
// unauthorized code here. The client has already authenticated
// successfully with basic auth and should just
// get back the invalid token error.
#SuppressWarnings("serial")
InvalidTokenException e400 = new InvalidTokenException(e.getMessage()) {
#Override
public int getHttpErrorCode() {
return 400;
}
};
return exceptionTranslator.translate(e400);
}
}
I'm currently working on a Spring Boot-Application with OAuth2-Authentication. I have a local OAuth2-Server where I receive a token when posting username and password of the local database against in my case http://localhost:8080/v1/oauth/token using Spring Boot's UserDetails and UserService. Everything works fine and nice.
But now I want to enhance my program with Facebook social login and want either log in to my local OAuth2-Server or using the external Facebook-Server. I checked out the Spring Boot example https://spring.io/guides/tutorials/spring-boot-oauth2/ and adapted the idea of an SSO-Filter. Now I can login using my Facebook client and secret id, but I cannot access my restricted localhost-sites.
What I want is that the Facebook-Token "behaves" the same way as the locally generated tokens by for instance being part of my local token storage. I checked out several tutorials and other Stackoverflow questions but with no luck. Here is what I have so far with a custom Authorization-Server and I think I'm still missing something very basic to get the link between external Facebook- and internal localhost-Server:
#Configuration
public class OAuth2ServerConfiguration {
private static final String SERVER_RESOURCE_ID = "oauth2-server";
#Autowired
private TokenStore tokenStore;
#Bean
public TokenStore tokenStore() {
return new InMemoryTokenStore();
}
protected class ClientResources {
#NestedConfigurationProperty
private AuthorizationCodeResourceDetails client = new AuthorizationCodeResourceDetails();
#NestedConfigurationProperty
private ResourceServerProperties resource = new ResourceServerProperties();
public AuthorizationCodeResourceDetails getClient() {
return client;
}
public ResourceServerProperties getResource() {
return resource;
}
}
#Configuration
#EnableResourceServer
#EnableOAuth2Client
protected class ResourceServerConfiguration extends ResourceServerConfigurerAdapter {
#Value("${pia.requireauth}")
private boolean requireAuth;
#Override
public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
resources.tokenStore(tokenStore).resourceId(SERVER_RESOURCE_ID);
}
#Autowired
OAuth2ClientContext oauth2ClientContext;
#Bean
public FilterRegistrationBean oauth2ClientFilterRegistration(OAuth2ClientContextFilter filter) {
FilterRegistrationBean registration = new FilterRegistrationBean();
registration.setFilter(filter);
registration.setOrder(-100);
return registration;
}
#Bean
#ConfigurationProperties("facebook")
public ClientResources facebook() {
return new ClientResources();
}
private Filter ssoFilter() {
CompositeFilter filter = new CompositeFilter();
List<Filter> filters = new ArrayList<>();
filters.add(ssoFilter(facebook(), "/login/facebook"));
filter.setFilters(filters);
return filter;
}
private Filter ssoFilter(ClientResources client, String path) {
OAuth2ClientAuthenticationProcessingFilter filter = new OAuth2ClientAuthenticationProcessingFilter(path);
OAuth2RestTemplate template = new OAuth2RestTemplate(client.getClient(), oauth2ClientContext);
filter.setRestTemplate(template);
UserInfoTokenServices tokenServices = new UserInfoTokenServices(client.getResource().getUserInfoUri(),
client.getClient().getClientId());
tokenServices.setRestTemplate(template);
filter.setTokenServices(tokenServices);
return filter;
}
#Override
public void configure(HttpSecurity http) throws Exception {
if (!requireAuth) {
http.antMatcher("/**").authorizeRequests().anyRequest().permitAll();
} else {
http.antMatcher("/**").authorizeRequests().antMatchers("/admin/**").hasRole("ADMIN")
.antMatchers("/", "/login**", "/webjars/**").permitAll().anyRequest().authenticated().and()
.exceptionHandling().and().csrf()
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()).and()
.addFilterBefore(ssoFilter(), BasicAuthenticationFilter.class);
}
}
}
#Configuration
#EnableAuthorizationServer
protected class OAuth2Configuration extends AuthorizationServerConfigurerAdapter {
#Value("${pia.oauth.tokenTimeout:3600}")
private int expiration;
#Autowired
private AuthenticationManager authenticationManager;
#Autowired
#Qualifier("userDetailsService")
private UserDetailsService userDetailsService;
// password encryptor
#Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
#Override
public void configure(AuthorizationServerEndpointsConfigurer configurer) throws Exception {
configurer.authenticationManager(authenticationManager).tokenStore(tokenStore).approvalStoreDisabled();
configurer.userDetailsService(userDetailsService);
}
#Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.inMemory().withClient("pia").secret("alphaport").accessTokenValiditySeconds(expiration)
.authorities("ROLE_USER").scopes("read", "write").authorizedGrantTypes("password", "refresh_token")
.resourceIds(SERVER_RESOURCE_ID);
}
}
}
Any help and/or examples covering this issue greatly appreciated! :)
One possible solution is to implement the Authentication Filter and Authentication Provider.
In my case I've implemented an OAuth2 authentication and also permit the user to access some endpoints with facebook access_token
The Authentication Filter looks like this:
public class ServerAuthenticationFilter extends GenericFilterBean {
private BearerAuthenticationProvider bearerAuthenticationProvider;
private FacebookAuthenticationProvider facebookAuthenticationProvider;
public ServerAuthenticationFilter(BearerAuthenticationProvider bearerAuthenticationProvider,
FacebookAuthenticationProvider facebookAuthenticationProvider) {
this.bearerAuthenticationProvider = bearerAuthenticationProvider;
this.facebookAuthenticationProvider = facebookAuthenticationProvider;
}
#Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest httpRequest = (HttpServletRequest) request;
HttpServletResponse httpResponse = (HttpServletResponse) response;
Optional<String> authorization = Optional.fromNullable(httpRequest.getHeader("Authorization"));
try {
AuthType authType = getAuthType(authorization.get());
if (authType == null) {
SecurityContextHolder.clearContext();
httpResponse.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
}
String strToken = authorization.get().split(" ")[1];
if (authType == AuthType.BEARER) {
if (strToken != null) {
Optional<String> token = Optional.of(strToken);
logger.debug("Trying to authenticate user by Bearer method. Token: " + token.get());
processBearerAuthentication(token);
}
} else if (authType == AuthType.FACEBOOK) {
if (strToken != null) {
Optional<String> token = Optional.of(strToken);
logger.debug("Trying to authenticate user by Facebook method. Token: " + token.get());
processFacebookAuthentication(token);
}
}
logger.debug(getClass().getSimpleName() + " is passing request down the filter chain.");
chain.doFilter(request, response);
} catch (InternalAuthenticationServiceException internalAuthenticationServiceException) {
SecurityContextHolder.clearContext();
logger.error("Internal Authentication Service Exception", internalAuthenticationServiceException);
httpResponse.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
} catch (AuthenticationException authenticationException) {
SecurityContextHolder.clearContext();
httpResponse.sendError(HttpServletResponse.SC_UNAUTHORIZED, authenticationException.getMessage());
} catch (Exception e) {
SecurityContextHolder.clearContext();
e.printStackTrace();
httpResponse.sendError(HttpServletResponse.SC_UNAUTHORIZED, e.getMessage());
}
}
private AuthType getAuthType(String value) {
if (value == null)
return null;
String[] basicSplit = value.split(" ");
if (basicSplit.length != 2)
return null;
if (basicSplit[0].equalsIgnoreCase("bearer"))
return AuthType.BEARER;
if (basicSplit[0].equalsIgnoreCase("facebook"))
return AuthType.FACEBOOK;
return null;
}
private void processBearerAuthentication(Optional<String> token) {
Authentication resultOfAuthentication = tryToAuthenticateWithBearer(token);
SecurityContextHolder.getContext().setAuthentication(resultOfAuthentication);
}
private void processFacebookAuthentication(Optional<String> token) {
Authentication resultOfAuthentication = tryToAuthenticateWithFacebook(token);
SecurityContextHolder.getContext().setAuthentication(resultOfAuthentication);
}
private Authentication tryToAuthenticateWithBearer(Optional<String> token) {
PreAuthenticatedAuthenticationToken requestAuthentication = new PreAuthenticatedAuthenticationToken(token,
null);
return tryToAuthenticateBearer(requestAuthentication);
}
private Authentication tryToAuthenticateWithFacebook(Optional<String> token) {
PreAuthenticatedAuthenticationToken requestAuthentication = new PreAuthenticatedAuthenticationToken(token,
null);
return tryToAuthenticateFacebook(requestAuthentication);
}
private Authentication tryToAuthenticateBearer(Authentication requestAuthentication) {
Authentication responseAuthentication = bearerAuthenticationProvider.authenticate(requestAuthentication);
if (responseAuthentication == null || !responseAuthentication.isAuthenticated()) {
throw new InternalAuthenticationServiceException(
"Unable to Authenticate for provided credentials.");
}
logger.debug("Application successfully authenticated by bearer method.");
return responseAuthentication;
}
private Authentication tryToAuthenticateFacebook(Authentication requestAuthentication) {
Authentication responseAuthentication = facebookAuthenticationProvider.authenticate(requestAuthentication);
if (responseAuthentication == null || !responseAuthentication.isAuthenticated()) {
throw new InternalAuthenticationServiceException(
"Unable to Authenticate for provided credentials.");
}
logger.debug("Application successfully authenticated by facebook method.");
return responseAuthentication;
}
}
This, filters Authorization headers, identifies whether they are facebook or bearer and then directs to specific provider.
The Facebook Provider looks like this:
public class FacebookAuthenticationProvider implements AuthenticationProvider {
#Value("${config.oauth2.facebook.resourceURL}")
private String facebookResourceURL;
private static final String PARAMETERS = "fields=name,email,gender,picture";
#Autowired
FacebookUserRepository facebookUserRepository;
#Autowired
UserRoleRepository userRoleRepository;
#SuppressWarnings({ "rawtypes", "unchecked" })
#Override
public Authentication authenticate(Authentication auth) throws AuthenticationException {
Optional<String> token = auth.getPrincipal() instanceof Optional ? (Optional) auth.getPrincipal() : null;
if (token == null || !token.isPresent() || token.get().isEmpty())
throw new BadCredentialsException("Invalid Grants");
SocialResourceUtils socialResourceUtils = new SocialResourceUtils(facebookResourceURL, PARAMETERS);
SocialUser socialUser = socialResourceUtils.getResourceByToken(token.get());
if (socialUser != null && socialUser.getId() != null) {
User user = findOriginal(socialUser.getId());
if (user == null)
throw new BadCredentialsException("Authentication failed.");
Credentials credentials = new Credentials();
credentials.setId(user.getId());
credentials.setUsername(user.getEmail());
credentials.setName(user.getName());
credentials.setRoles(parseRoles(user.translateRoles()));
credentials.setToken(token.get());
return new UsernamePasswordAuthenticationToken(credentials, credentials.getId(),
parseAuthorities(getUserRoles(user.getId())));
} else
throw new BadCredentialsException("Authentication failed.");
}
protected User findOriginal(String id) {
FacebookUser facebookUser = facebookUserRepository.findByFacebookId(facebookId);
return null == facebookUser ? null : userRepository.findById(facebookUser.getUserId()).get();
}
protected List<String> getUserRoles(String id) {
List<String> roles = new ArrayList<>();
userRoleRepository.findByUserId(id).forEach(applicationRole -> roles.add(applicationRole.getRole()));
return roles;
}
private List<Roles> parseRoles(List<String> strRoles) {
List<Roles> roles = new ArrayList<>();
for(String strRole : strRoles) {
roles.add(Roles.valueOf(strRole));
}
return roles;
}
private Collection<? extends GrantedAuthority> parseAuthorities(Collection<String> roles) {
if (roles == null || roles.size() == 0)
return Collections.emptyList();
return roles.stream().map(role -> (GrantedAuthority) () -> "ROLE_" + role).collect(Collectors.toList());
}
#Override
public boolean supports(Class<?> auth) {
return auth.equals(UsernamePasswordAuthenticationToken.class);
}
}
The FacebookUser only makes a reference to the Local User Id and the Facebook Id (this is the link between facebook and our application).
This SocialResourceUtils is used to get the facebook user information via facebook API (using the method getResourceByToken). The facebook resource url is setted on application.properties (config.oauth2.facebook.resourceURL). This method is basically:
public SocialUser getResourceByToken(String token) {
RestTemplate restTemplate = new RestTemplate();
String authorization = token;
JsonNode response = null;
try {
response = restTemplate.getForObject(accessUrl + authorization, JsonNode.class);
} catch (RestClientException e) {
throw new BadCredentialsException("Authentication failed.");
}
return buildSocialUser(response);
}
The Bearer Provider is your local Authentication, you can make your own, or use the springboot defaults, use other authentication methods, idk (I will not put my implementation here, thats by you).
And finally you need to make your Web Security Configurer:
#ConditionalOnProperty("security.basic.enabled")
#Configuration
#EnableWebSecurity
#EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter {
#Autowired
private BearerAuthenticationProvider bearerAuthenticationProvider;
#Autowired
private FacebookAuthenticationProvider facebookAuthenticationProvider;
#Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable();
http.addFilterBefore(new ServerAuthenticationFilter(bearerAuthenticationProvider,
facebookAuthenticationProvider), BasicAuthenticationFilter.class);
}
}
Notice that it has the annotation ConditionalOnProperty to enable/disable on properties security.basic.enabled. The #EnableGlobalMethodSecurity(prePostEnabled = true) enables the usage of the annotation #PreAuthorize which enables us to protect endpoints by roles for example (using #PreAuthorize("hasRole ('ADMIN')") over an endpoint, to allow acces only to admins)
This code needs many improvements, but I hope I have helped.
In our spring boot application we use mitreid for openid connect. If we access a protected resource from browser, we are redirected to keycloak, our openid provider, and then after successful authentication, to the desired resource. This is working like intended.
Now we want to access the same resource from command line, e.g curl or httpie.
But nothing is working.
Getting access_token from keycloak returns an jwt with access_token and id_token.
But access with curl -H "Authorization: Bearer 'access_token>'" http://localhost:8080 doesn't work.
Here is my oidc configuration
public class OIDCSecurityConfiguration extends WebSecurityConfigurerAdapter {
public static final Logger LOG = LoggerFactory.getLogger(OIDCSecurityConfiguration.class);
private final String loginPath = OIDCAuthenticationFilter.FILTER_PROCESSES_URL;
#Value("${oidc.issuer-uri}")
private String issuerUri;
#Value("${oidc.client-uri}")
private String clientUri;
#Value("${oidc.client-id}")
private String clientId;
#Value("${oidc.client-secret}")
private String clientSecret;
#Value("${oidc.client-scopes}")
private String clientScopes;
private String getLogoutUri() {
final ServerConfiguration serverConfiguration = serverConfigurationService().getServerConfiguration(issuerUri);
if (serverConfiguration == null) {
LOG.error("OpenID Connect server configuration could not be retrieved");
return "/";
}
try {
return serverConfiguration.getEndSessionEndpoint() + "?redirect_uri=" + URLEncoder.encode(clientUri, "UTF-8");
} catch (UnsupportedEncodingException e) {
LOG.error("Cannot encode redirect uri");
return "/";
}
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.addFilterBefore(authenticationFilter(), AbstractPreAuthenticatedProcessingFilter.class)
.exceptionHandling().authenticationEntryPoint(authenticationEntryPoint())
.and()
.authorizeRequests()
.antMatchers("/logout").permitAll()
.and()
.formLogin().loginPage("/openid_connect_login")
.and()
.logout().logoutSuccessUrl(getLogoutUri());
}
#Autowired
public void configureAuthenticationProvider(AuthenticationManagerBuilder auth) throws Exception {
auth.authenticationProvider(authenticationProvider());
}
#Bean
public AuthenticationEntryPoint authenticationEntryPoint() {
return new LoginUrlAuthenticationEntryPoint(loginPath);
}
#Bean
public WebMvcConfigurerAdapter mvcInterceptor() {
return new UserInfoInterceptorAdapter();
}
#Bean
public AuthenticationProvider authenticationProvider() {
final OIDCExtendedAuthenticationProvider authenticationProvider = new OIDCExtendedAuthenticationProvider();
authenticationProvider.setAuthoritiesMapper(new KeycloakAuthoritiesMapper(clientId));
return authenticationProvider;
}
#Bean
public AbstractAuthenticationProcessingFilter authenticationFilter() throws Exception {
final OIDCAuthenticationFilter filter = new OIDCAuthenticationFilter();
filter.setAuthenticationManager(authenticationManager());
filter.setServerConfigurationService(serverConfigurationService());
filter.setClientConfigurationService(clientConfigurationService());
final StaticSingleIssuerService issuer = new StaticSingleIssuerService();
issuer.setIssuer(this.issuerUri);
filter.setIssuerService(issuer);
// TODO: change to signed or encrypted builder for production.
filter.setAuthRequestUrlBuilder(new PlainAuthRequestUrlBuilder());
return filter;
}
#Bean
public ClientConfigurationService clientConfigurationService() {
final StaticClientConfigurationService service = new StaticClientConfigurationService();
final RegisteredClient client = new RegisteredClient();
client.setClientId(clientId);
client.setClientSecret(clientSecret);
client.setClientName(clientId);
client.setScope(Arrays.stream(clientScopes.split(",")).collect(Collectors.toSet()));
client.setTokenEndpointAuthMethod(AuthMethod.SECRET_BASIC);
client.setRedirectUris(Collections.singleton(clientUri + loginPath));
client.setRequestObjectSigningAlg(JWSAlgorithm.RS256);
service.setClients(Collections.singletonMap(this.issuerUri, client));
return service;
}
#Bean
public ServerConfigurationService serverConfigurationService() {
final DynamicServerConfigurationService service = new DynamicServerConfigurationService();
service.setWhitelist(Collections.singleton(issuerUri));
return service;
}
private static class UserInfoInterceptorAdapter extends WebMvcConfigurerAdapter {
#Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new UserInfoInterceptor());
}
}
}