I'm consuming a set of oAuth2 protected services. It currently works like this: the client logs in using their username and password. I exchange these for a token. I keep the token in the session and submit it every time I want to call a service. It works, but the problem is that I do this completely manually, without using much of Spring Security oAuth2 support. Here's how it looks:
<!-- Configure Authentication mechanism -->
<authentication-manager alias="authenticationManager">
<authentication-provider ref="oAuth2AuthenticationProvider"/>
</authentication-manager>
<beans:bean id="oAuth2AuthenticationProvider" class="my.custom.Oauth2AuthenticationProvider">
<beans:constructor-arg name="accessTokenUri" value="http://x.x.x.x/oauth/token"/>
<beans:constructor-arg name="clientId" value="myClientId"/>
<beans:constructor-arg name="clientSecret" value="myClientSecret"/>
<beans:constructor-arg name="scope">
<beans:list>
<beans:value>myScope</beans:value>
</beans:list>
</beans:constructor-arg>
</beans:bean>
<beans:bean id="resourceOwnerPasswordAccessTokenProvider" class="org.springframework.security.oauth2.client.token.grant.password.ResourceOwnerPasswordAccessTokenProvider"/>
As you can see I made the authentication provider myself. It is accepting the standard UsernamePasswordAuthenticationToken
but is producing my own extension of it that keeps the actual OAuth2AccessToken
as well, thus keeping it in the security context.
public class Oauth2AuthenticationProvider implements AuthenticationProvider {
@Autowired
private ResourceOwnerPasswordAccessTokenProvider provider;
private String accessTokenUri;
private String clientId;
private String clientSecret;
private List<String> scope;
public Oauth2AuthenticationProvider(String accessTokenUri, String clientId, String clientSecret, List<String> scope) {
this.accessTokenUri = accessTokenUri;
this.clientId = clientId;
this.clientSecret = clientSecret;
this.scope = scope;
}
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
String username = authentication.getName();
String password = authentication.getCredentials().toString();
OAuth2AccessToken token = obtainToken(username, password);
return handleLogonSuccess(authentication, token);
}
private OAuth2AccessToken obtainToken(String username, String password) {
ResourceOwnerPasswordResourceDetails passwordResourceDetails = new ResourceOwnerPasswordResourceDetails();
passwordResourceDetails.setUsername(username);
passwordResourceDetails.setPassword(password);
passwordResourceDetails.setClientId(clientId);
passwordResourceDetails.setClientSecret(clientSecret);
passwordResourceDetails.setScope(scope);
passwordResourceDetails.setAccessTokenUri(accessTokenUri);
DefaultAccessTokenRequest defaultAccessTokenRequest = new DefaultAccessTokenRequest();
OAuth2AccessToken token;
try {
token = provider.obtainAccessToken(passwordResourceDetails, defaultAccessTokenRequest);
} catch (OAuth2AccessDeniedException accessDeniedException) {
throw new BadCredentialsException("Invalid credentials", accessDeniedException);
}
return token;
}
public OAuth2AccessToken refreshToken(OAuth2AuthenticationToken authentication) {
OAuth2AccessToken token = authentication.getoAuth2AccessToken();
OAuth2RefreshToken refreshToken = token.getRefreshToken();
BaseOAuth2ProtectedResourceDetails resourceDetails = new BaseOAuth2ProtectedResourceDetails();
resourceDetails.setClientId(clientId);
resourceDetails.setClientSecret(clientSecret);
resourceDetails.setScope(scope);
resourceDetails.setAccessTokenUri(accessTokenUri);
OAuth2AccessToken newToken = provider.refreshAccessToken(resourceDetails, refreshToken, new DefaultAccessTokenRequest());
authentication.setoAuth2AccessToken(newToken);
return newToken;
}
public boolean supports(Class<?> authentication) {
return UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication);
}
private Authentication handleLogonSuccess(Authentication authentication, OAuth2AccessToken token) {
MyCustomOAuth2AuthenticationToken successAuthenticationToken = new MyCustomOAuth2AuthenticationToken(user, authentication.getCredentials(), calculateAuthorities(authentication), token);
return successAuthenticationToken;
}
public list<GrantedAuthority> calculateAuthorities(Authentication authentication) {
//my custom logic that assigns the correct role. e.g. ROLE_USER
}
}
As you see, it basically makes sure the token remains in the security scope from where I can simply extract it manually before each call to the back-end services. Similarly, I will check the freshness of the token before each call. This works well, but I'm sure I can use Spring's oauth namespace in XML (I'm not using Java config) to achieve the same in a more-config-less-code way. Most examples I find include the oAuth server implementation which I do not care about and just confuse me.
Can anyone please help me with this?
I've mashed a similar solution from browsing the Spring Security OAuth sources and bits and pieces of other solutions found online. I'm using Java Config but maybe it can help you map to a xml configuration, here it goes:
@Configuration
@EnableOAuth2Client
public class RestClientConfig {
@Value("${http.client.maxPoolSize}")
private Integer maxPoolSize;
@Value("${oauth2.resourceId}")
private String resourceId;
@Value("${oauth2.clientId}")
private String clientId;
@Value("${oauth2.clientSecret}")
private String clientSecret;
@Value("${oauth2.accessTokenUri}")
private String accessTokenUri;
@Autowired
private OAuth2ClientContext oauth2ClientContext;
@Bean
public ClientHttpRequestFactory httpRequestFactory() {
return new HttpComponentsClientHttpRequestFactory(httpClient());
}
@Bean
public HttpClient httpClient() {
PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager();
connectionManager.setMaxTotal(maxPoolSize);
// This client is for internal connections so only one route is expected
connectionManager.setDefaultMaxPerRoute(maxPoolSize);
return HttpClientBuilder.create().setConnectionManager(connectionManager).build();
}
@Bean
public OAuth2ProtectedResourceDetails oauth2ProtectedResourceDetails() {
ResourceOwnerPasswordResourceDetails details = new ResourceOwnerPasswordResourceDetails();
details.setId(resourceId);
details.setClientId(clientId);
details.setClientSecret(clientSecret);
details.setAccessTokenUri(accessTokenUri);
return details;
}
@Bean
public AccessTokenProvider accessTokenProvider() {
ResourceOwnerPasswordAccessTokenProvider tokenProvider = new ResourceOwnerPasswordAccessTokenProvider();
tokenProvider.setRequestFactory(httpRequestFactory());
return new AccessTokenProviderChain(
Arrays.<AccessTokenProvider> asList(tokenProvider)
);
}
@Bean
public OAuth2RestTemplate restTemplate() {
OAuth2RestTemplate template = new OAuth2RestTemplate(oauth2ProtectedResourceDetails(), oauth2ClientContext);
template.setRequestFactory(httpRequestFactory());
template.setAccessTokenProvider(accessTokenProvider());
return template;
}
}
One important bit I found is that you need to use the AccessTokenProviderChain even for a single Provider otherwise the automatic token refresh (after authentication) won't work.
To set the user credentials on the first request you'll need this:
@Autowired
private OAuth2RestTemplate restTemplate;
restTemplate.getOAuth2ClientContext().getAccessTokenRequest().set("username", username);
restTemplate.getOAuth2ClientContext().getAccessTokenRequest().set("password", password);
Then you can issue requests as normal using the RestTemplate methods, e.g:
String url = "http://localhost:{port}/api/users/search/findByUsername?username={username}";
ResponseEntity<User> responseEntity = restTemplate.getForEntity(
url, User.class, 8081, username);
If you want to trace the requests on the wire you can set the log level on apache http client to DEBUG, e.g. with Spring Boot:
logging.level.org.apache.http=DEBUG
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With