I am using Spring-Security-OAuth2 for implementing my own oauth server and resource server. I am using RemoteTokenService as my ResourceServerTokenService on my ResourceServer which will authenticate any accessToken using the CheckTokenEndpoint (/oauth/check_token) on OAuth Server.
I have added a antMatcher for an api url e.g. /data/list which will need client application Role / Authority: "ROLE_ADMIN" like this .antMatcher('/data/list').access("#oauth2.clientHasRole('ROLE_ADMIN')")
but it is not working.
I have done some trial and error on this end point and what I get is following :::
When oauth grant is client only i.e. client_credential grant.
what we get from /oauth/check_token
{
"scope":["read"],
"exp":1412955393,
"client_id":"sample_test_client_app"
}
we dont get any client authority. so how can spring security will perform above authorization check of "#oauth2.clientHasRole('ROLE_ADMIN')"
When oauth grant is user + client i.e. Authorization_code grant
what we get from /oauth/check_token
{
"aud":["resource_id"],
"exp":1412957540,
"user_name":"developer",
"authorities":["ROLE_USER"],
"client_id":"sample_test_client_app",
"scope":["read"]
}
and for authorization_code grnat we are still not getting client authority/role. so can any one tell me how can we perform clientHasRole authentication on any api url?
For "#oauth2.clientHasRole('ROLE_ADMIN')" to work we have to implemented our AccessTokenConverter and inject it into auth server and resource server.
so create a new class which extends DefaultAccessTokenConverter and override convertAccessToken and extractAuthentication methods.
In convertAccessToken method just add
Map<String, Object> response = (Map<String, Object>) super.convertAccessToken(token, authentication);
OAuth2Request clientToken = authentication.getOAuth2Request();
response.put("clientAuthorities", clientToken.getAuthorities());
and in extractAuthentication method add
Collection<HashMap<String, String>> clientAuthorities = (Collection<HashMap<String, String>>) map.get("client_authority");
Collection<GrantedAuthority> grantedAuthorities = new ArrayList<GrantedAuthority>();
for (HashMap<String, String> grantedAuthority : clientAuthorities) {
for (String authority : grantedAuthority.values()) {
grantedAuthorities.add(new SimpleGrantedAuthority(authority));
}
}
Set<String> resourceIds = new LinkedHashSet<String>(map.containsKey(AUD) ? (Collection<String>) map.get(AUD) : Collections.<String> emptySet());
OAuth2Request request = new OAuth2Request(parameters, clientId, grantedAuthorities, true, scope, resourceIds, null, null, null);
At auth server :
set this class in AuthorizationServerEndpointsConfigurer
At resource server :
set this class in RemoteTokenServices
Related
I have a Spring Boot application that users Spring Security. My Authentication and Authorization filters are working as expected.
In my Authentication filter, I generate JWT token with list of user authorities set as claim, and send the generated JWT together with claims back to client as part of "Auth" header. That is all working great.
In Authorization filter, I also got it all working fine, my doFilterInternals() override does proper chaining and it also calls my getAuthenticationToken() method:
private UsernamePasswordAuthenticationToken getAuthenticationToken(HttpServletRequest request) {
String token = request.getHeader("Auth");
if (token != null) {
token = token.replace("Bearer", "");
String username = Jwts.parser()
.setSigningKey(SecurityConstants.getTokenSecret())
.parseClaimsJws(token)
.getBody()
.getSubject();
String authoritiesString = Jwts.parser()
.setSigningKey(SecurityConstants.getTokenSecret())
.parseClaimsJws(token)
.getBody()
.get("user-authorities").toString(); //authority1, authority2, ...
if (username != null) {
List<GrantedAuthority> authorities = AuthorityUtils.commaSeparatedStringToAuthorityList(authoritiesString);
return new UsernamePasswordAuthenticationToken(username, null, authorities);
}
}
return null;
}
Above, I extract authorities (these are user groups coming from Active Directory) from claim I named"user-authorities" and I generate new UsernamePasswordAuthenticationToken with the authorities and return it.
This is all working great and I have been using for a while now.
Now, I am trying to use these authorities to add method level security to my controllers.
In order to do so, I have a #Configuration class which I annotated with #EnableGlobalMethodSecurity:
#Configuration
#EnableGlobalMethodSecurity(
prePostEnabled = true,
securedEnabled = true,
jsr250Enabled = true
)
public class AppConfig {
...
}
Then on my controller I am using #Secured("authority1") to secure my controller:
#PostMapping(consumes = {MediaType.APPLICATION_JSON_VALUE}, produces = {MediaType.APPLICATION_JSON_VALUE})
#Secured("authority1")
public CarResponse saveCar(#Valid #RequestBody CarRequest carRequest, #RequestHeader HttpHeaders httpHeaders) {
System.out.println("Received :" + carRequest.toString());
return null;
}
I know JWT token contains claims with "authority1,authority2,authority3" comma-delimited string of authorities. So, my expectation would be that the controller below will execute for a user who authenticates and has these 3 authorities.
However, what I get back is 500 error. If I comment out the #Secured annotation, my controller will execute just fine but then it is not secured. I have also tried using #PreAuthorized("hasRole('authority1')") and also #RolesAllowed("authority1") but none are workng.
I dont know what I am missing.
I am using Spring Oauth2 and ADFS for security purpose. However I can not find the endpoint for checking token from response of ADFS.
I also have Spring Authorization Provider which is written in Java. And my application called it by using these properties:
security.oauth2.client.clientId=myclient
security.oauth2.client.client-secret= mysecret
security.oauth2.client.userAuthorizationUri= http://127.0.0.1:9999/oauth/authorize?resource=https://localhost:8443/login
security.oauth2.client.accessTokenUri= http://127.0.0.1:9999/oauth/token
security.oauth2.resource.user-info-uri= http://127.0.0.1:9999/login
security.oauth2.resource.token-info-uri= http://127.0.0.1:9999/oauth/check_token
security.oauth2.client.tokenName=code
security.oauth2.client.authenticationScheme=query
security.oauth2.client.clientAuthenticationScheme=form
security.oauth2.client.grant-type=authorization_code
And I have changed the values of the properties to connect with ADFS
security.oauth2.client.clientId=myclient
security.oauth2.client.client-secret= myclient
security.oauth2.client.userAuthorizationUri= https://adfs.local/adfs/oauth2/authorize?resource=https://localhost:8443/login
security.oauth2.client.accessTokenUri= https://adfs.local/adfs/oauth2/token
security.oauth2.resource.user-info-uri= https://adfs.local/adfs/oauth2/userinfo
security.oauth2.resource.token-info-uri= https://adfs.local/adfs/oauth2/check_token
security.oauth2.client.tokenName=code
security.oauth2.client.authenticationScheme=query
security.oauth2.client.clientAuthenticationScheme=form
security.oauth2.client.grant-type=authorization_code
However, I found that https://adfs.local/adfs/oauth2/check_token is invalid in ADFS.
How can I get the check_token in ADFS? check_token is Token Introspection Endpoint, however, this endpoint doesn't return node 'active' according to OAuth 2 Extension which is mandatory. See this link
This is what Spring Authorization Provider do when return check_token endpoint
#RequestMapping(value = "/oauth/check_token", method = RequestMethod.POST)
#ResponseBody
public Map<String, ?> checkToken(#RequestParam("token") String value) {
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, Object> response = (Map<String, Object>)accessTokenConverter.convertAccessToken(token, authentication);
// gh-1070
response.put("active", true); // Always true if token exists and not expired
return response;
}
ADFS has no such endpoint and I don't believe it's part of the spec?
You could use:
https://[Your ADFS hostname]/adfs/.well-known/openid-configuration
to get the keys to check the JWT yourself which is the usual practice.
There are many resources on how to check the JWT e.g. this.
I am trying to authenticate the user using the password flow in the latest version of Spring Security - 5.2.
The docs seem to suggest how to do that.
#Bean
public OAuth2AuthorizedClientManager passwordFlowAuthorizedClientManager(
HttpClient httpClient,
ClientRegistrationRepository clientRegistrationRepository,
OAuth2AuthorizedClientRepository authorizedClientRepository) {
HttpComponentsClientHttpRequestFactory requestFactory = new HttpComponentsClientHttpRequestFactory();
requestFactory.setHttpClient(httpClient);
DefaultPasswordTokenResponseClient c = new DefaultPasswordTokenResponseClient();
RestTemplate client = new RestTemplate(requestFactory);
client.setMessageConverters(Arrays.asList(
new FormHttpMessageConverter(),
new OAuth2AccessTokenResponseHttpMessageConverter()));
client.setErrorHandler(new OAuth2ErrorResponseErrorHandler());
c.setRestOperations(client);
OAuth2AuthorizedClientProvider authorizedClientProvider = OAuth2AuthorizedClientProviderBuilder.builder()
.password(configurer -> configurer.accessTokenResponseClient(c))
.refreshToken()
.build();
DefaultOAuth2AuthorizedClientManager authorizedClientManager =
new DefaultOAuth2AuthorizedClientManager(
clientRegistrationRepository, authorizedClientRepository);
authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider);
authorizedClientManager.setContextAttributesMapper(authorizeRequest -> {
Map<String, Object> contextAttributes = new HashMap<>();
String username = authorizeRequest.getAttribute(OAuth2ParameterNames.USERNAME);
String password = authorizeRequest.getAttribute(OAuth2ParameterNames.PASSWORD);
contextAttributes.put(OAuth2AuthorizationContext.USERNAME_ATTRIBUTE_NAME, username);
contextAttributes.put(OAuth2AuthorizationContext.PASSWORD_ATTRIBUTE_NAME, password);
return contextAttributes;
});
return authorizedClientManager;
}
I execute the request, I can see the access token returned in HTTP header but the SecurityContext is not populated and the session user remains anonymous.
String username = "joe";
String password = "joe";
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
ClientRegistration r = clientRegistrationRepository.findByRegistrationId("keycloak");
OAuth2AuthorizeRequest authorizeRequest = OAuth2AuthorizeRequest.withClientRegistrationId(r.getRegistrationId())
.principal(authentication)
.attributes(attrs -> {
attrs.put(OAuth2ParameterNames.USERNAME, username);
attrs.put(OAuth2ParameterNames.PASSWORD, password);
})
.build();
OAuth2AuthorizedClient authorizedClient = this.authorizedClientManager.authorize(authorizeRequest);
Any ideas?
After reading into the documentation a bit more I do not think that Oauth 2 password flow in Spring Security 5.2 is supported the same way authorisation flow is. Spring Security 5.2 has password flow support for the http client which can cache the authorization request and refresh the token before it expires - but there is no end user password flow support in which the client proxies the credentials to the authorization server.
Of course, it is entirely possible to authenticate the end user by harvesting the credentials, implementing a custom AuthenticationProvider that swaps the credentials for a token with the authorization server and returns an OAuth2AuthenticationToken that is persisted to the context.
On my current project I have an app that has a small graphical piece that users authenticate using SSO, and a portion that is purely API where users authenticate using an Authorization header.
For example:
/ping-other-service is accessed using SSO.
/api/ping-other-service is accessed using a bearer token
Being all cloud native our app communicates with other services that uses the same SSO provider using JWT tokens (UAA), so I figured we'd use OAuth2RestTemplate since according to the documentation it can magically insert the authentication credentials. It does do that for all endpoints that are authenticated using SSO. But when we use an endpoint that is authed through bearer token it doesn't populate the rest template.
My understanding from the documentation is that #EnableOAuth2Client will only extract the token from a SSO login, not auth header?
What I'm seeing
Failed request and what it does:
curl -H "Authorization: Bearer <token>" http://localhost/api/ping-other-service
Internally uses restTemplate to call http://some-other-service/ping which responds 401
Successful request and what it does:
Chrome http://localhost/ping-other-service
Internally uses restTemplate to call http://some-other-service/ping which responds 200
How we worked around it
To work around this I ended up creating the following monstrosity which will extract the token from the OAuth2ClientContext if it isn't available from an authorization header.
#PostMapping(path = "/ping-other-service")
public ResponseEntity ping(#PathVariable String caseId, HttpServletRequest request, RestTemplate restTemplate) {
try {
restTemplate.postForEntity(adapterUrl + "/webhook/ping", getRequest(request), Map.class);
} catch (HttpClientErrorException e) {
e.printStackTrace();
return new ResponseEntity(HttpStatus.SERVICE_UNAVAILABLE);
}
return new ResponseEntity(HttpStatus.OK);
}
private HttpEntity<?> getRequest(HttpServletRequest request) {
HttpHeaders headers = new HttpHeaders();
headers.set("Authorization", "Bearer " + getRequestToken(request));
return new HttpEntity<>(null, headers);
}
private String getRequestToken(HttpServletRequest request) {
Authentication token = new BearerTokenExtractor().extract(request);
if (token != null) {
return (String) token.getPrincipal();
} else {
OAuth2AccessToken accessToken = oAuth2ClientContext.getAccessToken();
if (accessToken != null) {
return accessToken.getValue();
}
}
throw new ResourceNotFound("No valid access token found");
}
In the /api/** resources there is an incoming token, but because you are using JWT the resource server can authenticate without calling out to the auth server, so there is no OAuth2RestTemplate just sitting around waiting for you to re-use the context in the token relay (if you were using UserInfoTokenServices there would be one). You can create one though quite easily, and pull the incoming token out of the SecurityContext. Example:
#Autowired
private OAuth2ProtectedResourceDetails resource;
private OAuth2RestTemplate tokenRelayTemplate(Principal principal) {
OAuth2Authentication authentication = (OAuth2Authentication) principal;
OAuth2AuthenticationDetails details = (OAuth2AuthenticationDetails) authentication.getDetails();
details.getTokenValue();
OAuth2ClientContext context = new DefaultOAuth2ClientContext(new DefaultOAuth2AccessToken(details.getTokenValue()));
return new OAuth2RestTemplate(resource, context);
}
You could probably turn that method into #Bean (in #Scope("request")) and inject the template with a #Qualifier if you wanted.
There's some autoconfiguration and a utility class to help with this pattern in Spring Cloud Security, e.g: https://github.com/spring-cloud/spring-cloud-security/blob/master/spring-cloud-security/src/main/java/org/springframework/cloud/security/oauth2/client/AccessTokenContextRelay.java
I came across this problem when developing a Spring resource server, and I needed to pass the OAuth2 token from a request to the restTemplate for a call to a downstream resource server. Both resource servers use the same auth server, and I found Dave's link helpful but I had to dig a bit to find out how to implement this. I ended up finding the documentation here, and it turn's out the implemetation was very simple. I was using #EnableOAuth2Client, so I had to create the restTemplate bean with the injected OAuth2ClientContext and create the appropriate resource details. In my case it was ClientCredentialsResourceDetails. Thanks for all great work Dave!
#Bean
public OAuth2RestOperations restTemplate (OAuth2ClientContext context) {
ClientCredentialsResourceDetails details = new ClientCredentialsResourceDetails();
// Configure the details here
return new OAuth2RestTemplate(details, context)
}
#Dave Syer
My UAA service is also an oauth2 client, which needs to relay JWT tokens coming in from Zuul. When configuring the oauth2 client the following way
#Configuration
#EnableOAuth2Client
#RibbonClient(name = "downstream")
public class OAuthClientConfiguration {
#Bean
public OAuth2RestTemplate restTemplate(OAuth2ProtectedResourceDetails resource, OAuth2ClientContext context) {
return new OAuth2RestTemplate(resource, context);
}
}
I do get a 401 response from the downstream service as my access token has a very short validity and the AccessTokenContextRelay does not update an incoming access token (Zuul does renew expired access tokens by the refresh token).
The OAuth2RestTemplate#getAccessToken will never acquire a new access token as the isExpired on the access token stored by the AccessTokenContextRelay drops the validity and refresh token information.
How can this by solved?
In my Spring Boot application I need to programmatically create a new user and obtain OAuth2 access/refresh tokens for him from internal(part of this application) OAuth2 Authorization Server.
Then, I plan to send these access/refresh tokens to some external (client) application that will interact with my first application on behalf of this user.
Is it possible to programmatically obtain OAuth2 access/refresh tokens for this user without providing password(during the programmatic creation of this user I don't want to deal with password, only username).
Yes you can, take a look at the code below
#Autowired
private TokenEndpoint tokenEndpoint;
public ResponseEntity<?> createToken(User user) {
Principal principal = new UsernamePasswordAuthenticationToken(user.getUserName(), user.getPassword(), user.getAuthorities());
HashMap<String, String> parameters = new HashMap<String, String>();
parameters.put("client_id", "XXX");
parameters.put("client_secret", "XXX");
parameters.put("grant_type", "password");
parameters.put("password", user.getPassword());
parameters.put("scope", "XXX");
parameters.put("username", user.getUserName());
return tokenEndpoint.getAccessToken(principal, parameters);
}
but you are violating the OAuth2 spec. Authorization should be performed by Resource Owner.