Skip to content

Instantly share code, notes, and snippets.

@ccampo133
Created December 16, 2014 23:20
Show Gist options
  • Save ccampo133/c3ae35a1fe5bfb794cc7 to your computer and use it in GitHub Desktop.
Save ccampo133/c3ae35a1fe5bfb794cc7 to your computer and use it in GitHub Desktop.
@Configuration
protected static class CustomAuthorizationServerSecurityConfig extends AuthorizationServerSecurityConfiguration {
@Autowired
private AuthorizationServerEndpointsConfiguration endpoints;
@Autowired
private ClientDetailsService clientDetailsService;
@Override
protected void configure(final HttpSecurity http) throws Exception {
final AuthorizationServerSecurityConfigurer configurer = new AuthorizationServerSecurityConfigurer();
final FrameworkEndpointHandlerMapping handlerMapping = endpoints.oauth2EndpointHandlerMapping();
http.setSharedObject(FrameworkEndpointHandlerMapping.class, handlerMapping);
configure(configurer);
http.apply(configurer);
final String tokenEndpointPath = handlerMapping.getServletPath("/oauth/token");
final String tokenKeyPath = handlerMapping.getServletPath("/oauth/token_key");
final String checkTokenPath = handlerMapping.getServletPath("/oauth/check_token");
// @formatter:off
http
.authorizeRequests()
// Per the OAuth2 spec, we should only allow POST to the token endpoint.
// See: https://github.com/spring-projects/spring-security-oauth/issues/327
.antMatchers(HttpMethod.GET, tokenEndpointPath).denyAll()
.antMatchers(HttpMethod.PUT, tokenEndpointPath).denyAll()
.antMatchers(HttpMethod.PATCH, tokenEndpointPath).denyAll()
.antMatchers(HttpMethod.DELETE, tokenEndpointPath).denyAll()
.and()
.authorizeRequests()
.antMatchers(tokenEndpointPath).fullyAuthenticated()
// Secure the token revocation endpoint so OAuth2 clients can revoke tokens.
.antMatchers("/oauth/revoke")
.hasAuthority("ROLE_CLIENT")
.antMatchers(tokenKeyPath)
.access(configurer.getTokenKeyAccess())
.antMatchers(checkTokenPath)
.access(configurer.getCheckTokenAccess())
.and()
.requestMatchers()
.antMatchers(tokenEndpointPath, tokenKeyPath, checkTokenPath, "/oauth/revoke");
// @formatter:on
http.setSharedObject(ClientDetailsService.class, clientDetailsService);
}
}
@RestController
public class OAuthController {
private static final Logger log = LoggerFactory.getLogger(OAuthController.class);
private final OAuthService oAuthService;
private final WebResponseExceptionTranslator exceptionTranslator;
@Autowired
public OAuthController(final OAuthService oAuthService, final WebResponseExceptionTranslator exceptionTranslator) {
this.oAuthService = oAuthService;
this.exceptionTranslator = exceptionTranslator;
}
/**
* This endpoint should ALWAYS be restricted to authenticated OAuth clients (has role ROLE_CLIENT).
* See {@link com.voxeo.armrest.config.OAuth2Configuration.CustomAuthorizationServerSecurityConfig},
* where this restriction is explicitly enforced. This controller endpoint follows the spec defined
* in <a href="https://tools.ietf.org/html/rfc7009#section-2.2">RFC 7009 - OAuth 2.0 Token Revocation</a>.
*/
@RequestMapping(value = "/oauth/revoke", method = RequestMethod.POST)
public ResponseEntity<Void> revokeToken(@RequestParam("token") final String token,
@RequestParam(value = "token_hint", required = false) final String tokenHint, final Principal principal) {
log.info("POST {}, /oauth/revoke; token = {}, tokenHint = {}", token, tokenHint);
// Invalid token revocations (token does not exist) still respond
// with HTTP 200. Still, log the result anyway for posterity.
// See: https://tools.ietf.org/html/rfc7009#section-2.2
if (!oAuthService.revokeToken(token, tokenHint, (Authentication) principal)) {
log.debug("No token with value {} was revoked.", token);
}
return new ResponseEntity<>(HttpStatus.OK);
}
@ExceptionHandler(OAuth2Exception.class)
public ResponseEntity<OAuth2Exception> handleException(Exception e) throws Exception {
log.info("Handling error: {}, {}", e.getClass().getSimpleName(), e.getMessage());
return exceptionTranslator.translate(e);
}
}
Service
public class OAuthService {
private static final Logger log = LoggerFactory.getLogger(OAuthService.class);
private final TokenStore tokenStore;
@Autowired
public OAuthService(final TokenStore tokenStore) {
this.tokenStore = tokenStore;
}
public boolean revokeToken(final String token, final String tokenHint, final Authentication clientAuth) {
log.debug("revokeToken; token = {}, tokenHint = {}, clientAuth = {}", token, tokenHint, clientAuth);
// Check the refresh_token store first. Fall back to the access token store if we don't
// find anything. See RFC 7009, Sec 2.1: https://tools.ietf.org/html/rfc7009#section-2.1
if (tokenHint != null && tokenHint.equals("refresh_token")) {
return revokeRefreshToken(token, clientAuth) || revokeAccessToken(token, clientAuth);
}
// The user didn't hint that this is a refresh token, so it MAY be an access
// token. If we don't find an access token... check if it's a refresh token.
return revokeAccessToken(token, clientAuth) || revokeRefreshToken(token, clientAuth);
}
private boolean revokeRefreshToken(final String token, final Authentication clientAuth) {
final OAuth2RefreshToken refreshToken = tokenStore.readRefreshToken(token);
if (refreshToken != null) {
log.debug("Found refresh token {}.", token);
final OAuth2Authentication authToRevoke = tokenStore.readAuthenticationForRefreshToken(refreshToken);
checkIfTokenIsIssuedToClient(clientAuth, authToRevoke);
tokenStore.removeAccessTokenUsingRefreshToken(refreshToken);
tokenStore.removeRefreshToken(refreshToken);
log.debug("Successfully removed refresh token {} (and any associated access token).", refreshToken);
return true;
}
log.debug("No refresh token {} found in the token store.", token);
return false;
}
private boolean revokeAccessToken(final String token, final Authentication clientAuth) {
final OAuth2AccessToken accessToken = tokenStore.readAccessToken(token);
if (accessToken != null) {
log.debug("Found access token {}.", token);
final OAuth2Authentication authToRevoke = tokenStore.readAuthentication(accessToken);
checkIfTokenIsIssuedToClient(clientAuth, authToRevoke);
if (accessToken.getRefreshToken() != null) {
tokenStore.removeRefreshToken(accessToken.getRefreshToken());
}
tokenStore.removeAccessToken(accessToken);
log.debug("Successfully removed access token {} (and any associated refresh token).", token);
return true;
}
log.debug("No access token {} found in the token store.", token);
return false;
}
private void checkIfTokenIsIssuedToClient(final Authentication clientAuth,
final OAuth2Authentication authToRevoke) {
final String requestingClientId = clientAuth.getName();
final String tokenClientId = authToRevoke.getOAuth2Request().getClientId();
if (!requestingClientId.equals(tokenClientId)) {
log.debug("Revoke FAILED: requesting client = {}, token's client = {}.", requestingClientId, tokenClientId);
throw new InvalidGrantException("Cannot revoke tokens issued to other clients.");
}
log.debug("OK to revoke; token is issued to client \"{}\"", requestingClientId);
}
}
@uzubair1
Copy link

uzubair1 commented May 6, 2016

We are integrating this piece of code. We are facing issue on triggering request for /oauth/revoke.

We are using /oauth/authorize?response_type=code to go to login page. Then getting token by /oauth/token by sending grant_type=authorization_code and Auth Header as 'Basic acme:acmesecret.

In table oauth_access_token, client_id 'acme' is being set and user_name 'john'.

We call /user to get UserName by sending Auth Header Bearer token

How should we call /oauth/revoke?

When we call it by sending Auth Header as Basic acme:acmesecret, it gives following error

"NetworkError: 401 Unauthorized - http://192.168.30.170:9999/uaa/oauth/revoke"

`2016-05-06 10:08:18.835 DEBUG 6121 --- [io-9999-exec-10] o.s.security.web.FilterChainProxy : /oauth/revoke at position 9 of 11 in additional filter chain; firing Filter: 'SessionManagementFilter'
2016-05-06 10:08:18.835 DEBUG 6121 --- [io-9999-exec-10] o.s.security.web.FilterChainProxy : /oauth/revoke at position 10 of 11 in additional filter chain; firing Filter: 'ExceptionTranslationFilter'
2016-05-06 10:08:18.835 DEBUG 6121 --- [io-9999-exec-10] o.s.security.web.FilterChainProxy : /oauth/revoke at position 11 of 11 in additional filter chain; firing Filter: 'FilterSecurityInterceptor'
2016-05-06 10:08:18.835 DEBUG 6121 --- [io-9999-exec-10] o.s.s.w.a.i.FilterSecurityInterceptor : Secure object: FilterInvocation: URL: /oauth/revoke; Attributes: [#oauth2.throwOnError(authenticated)]
2016-05-06 10:08:18.835 DEBUG 6121 --- [io-9999-exec-10] o.s.s.w.a.i.FilterSecurityInterceptor : Previously Authenticated: org.springframework.security.authentication.AnonymousAuthenticationToken@9055e4a6: Principal: anonymousUser; Credentials: [PROTECTED]; Authenticated: true; Details: org.springframework.security.web.authentication.WebAuthenticationDetails@957e: RemoteIpAddress: 192.168.30.170; SessionId: null; Granted Authorities: ROLE_ANONYMOUS
2016-05-06 10:08:18.835 DEBUG 6121 --- [io-9999-exec-10] o.s.s.access.vote.AffirmativeBased : Voter: org.springframework.security.web.access.expression.WebExpressionVoter@139141a0, returned: -1
2016-05-06 10:08:18.836 DEBUG 6121 --- [io-9999-exec-10] o.s.s.w.a.ExceptionTranslationFilter : Access is denied (user is anonymous); redirecting to authentication entry point

org.springframework.security.access.AccessDeniedException: Access is denied
at org.springframework.security.access.vote.AffirmativeBased.decide(AffirmativeBased.java:83) ~[spring-security-core-4.0.3.RELEASE.jar:4.0.3.RELEASE]
at org.springframework.security.access.intercept.AbstractSecurityInterceptor.beforeInvocation(AbstractSecurityInterceptor.java:232) ~[spring-security-core-4.0.3.RELEASE.jar:4.0.3.RELEASE]
at org.springframework.security.web.access.intercept.FilterSecurityInterceptor.invoke(FilterSecurityInterceptor.java:123) ~[spring-security-web-4.0.3.RELEASE.jar:4.0.3.RELEASE]
at org.springframework.security.web.access.intercept.FilterSecurityInterceptor.doFilter(FilterSecurityInterceptor.java:90) ~[spring-
...
2016-05-06 10:08:18.837 DEBUG 6121 --- [io-9999-exec-10] o.s.s.w.a.ExceptionTranslationFilter : Calling Authentication entry point.
2016-05-06 10:08:18.843 DEBUG 6121 --- [io-9999-exec-10] s.s.o.p.e.DefaultOAuth2ExceptionRenderer : Written [error="unauthorized", error_description="Full authentication is required to access this resource"] as "application/json" using [org.springframework.http.converter.json.MappingJackson2HttpMessageConverter@52577106]
2016-05-06 10:08:18.843 DEBUG 6121 --- [io-9999-exec-10] s.s.w.c.SecurityContextPersistenceFilter : SecurityContextHolder now cleared, as request processing completed
`

And when we call it by using Auth Header as Bearer token

"NetworkError: 500 Internal Server Error - http://192.168.30.170:9999/uaa/oauth/revoke"
`2016-05-06 10:15:17.455 DEBUG 6220 --- [nio-9999-exec-9] o.s.security.web.FilterChainProxy : /oauth/revoke reached end of additional filter chain; proceeding with original chain
2016-05-06 10:15:17.462 INFO 6220 --- [nio-9999-exec-9] c.e.authserver.config.OAuthController : POST eyJhbGciOiJSUzI1NiJ9.eyJleHAiOjE0NjI1MTUyODksInVzZXJfbmFtZSI6ImpvaG4iLCJzY29wZSI6WyJvcGVuaWQiXSwiYXV0aG9yaXRpZXMiOlsiUk9MRV9DTElFTlQiXSwiYXVkIjpbInNwcmluZy1ib290LWFwcGxpY2F0aW9uIl0sImp0aSI6ImM2Zjc4NDY4LWZkYzAtNDRjZi1iMjM4LWY5ODgxOGU3NTllYSIsImNsaWVudF9pZCI6InNkZSJ9.aaGTsN-tqy2Tihi5ssxW-7FpRyDyNC8y4ZEwZdzr5g8xdR8lAd1Q69KTinsl5ZJynedrqZeRZvzumUY8n0s-iL4DwDsRtgxLOxAPgLdfzT_uShMn2cLVflMUDbZzxyeIZSwY-oh2P86KkzcMvVmaIMTZ-35xMHGBDdV_pzIz-tUQwmRPPgj-2WXHTgy0qWGh8GSv-cv-gG_KkDNXOpTWsTCKrRS2LxZYFsElIFS38wvCEtn_U7aKuNw7JSd-QL-mKKJ1yxMnoUvbGThwUd9IYI0uXUjQEsPIZcRMStH88nmYvfzQNcIr3Tqb2Ty3kQD4cSrSYauwUAsUkl8dPO8rAg, /oauth/revoke; token = null, tokenHint = {}
2016-05-06 10:15:17.497 DEBUG 6220 --- [nio-9999-exec-9] s.s.w.c.SecurityContextPersistenceFilter : SecurityContextHolder now cleared, as request processing completed
2016-05-06 10:15:17.499 ERROR 6220 --- [nio-9999-exec-9] o.a.c.c.C.[.[.[.[dispatcherServlet] : Servlet.service() for servlet [dispatcherServlet] in context with path [/uaa] threw exception [Request processing failed; nested exception is error="invalid_grant", error_description="Cannot revoke tokens issued to other clients.john:acme"] with root cause

org.springframework.security.oauth2.common.exceptions.InvalidGrantException: Cannot revoke tokens issued to other clients.john:acme
at com.etilize.authserver.service.OAuthService.checkIfTokenIsIssuedToClient(OAuthService.java:80) ~[classes/:na]
at com.etilize.authserver.service.OAuthService.revokeAccessToken(OAuthService.java:60) ~[classes/:na]
at com.etilize.authserver.service.OAuthService.revokeToken(OAuthService.java:36) ~[classes/:na]
at com.etilize.authserver.config.OAuthController.revokeToken(OAuthController.java:49) ~[classes/:na]
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:1.7.0_67]
`

In following method tokenClientId is acme and requestingclientid is john.

private void checkIfTokenIsIssuedToClient(final Authentication clientAuth, final OAuth2Authentication authToRevoke) { final String requestingClientId = clientAuth.getName(); final String tokenClientId = authToRevoke.getOAuth2Request().getClientId(); log.debug("Revoke FAILED: requesting client = {}, token's client = {}.", requestingClientId, tokenClientId); if (!requestingClientId.equals(tokenClientId)) { log.debug("Revoke FAILED: requesting client = {}, token's client = {}.", requestingClientId, tokenClientId); throw new InvalidGrantException("Cannot revoke tokens issued to other clients."+requestingClientId+":"+tokenClientId); } log.debug("OK to revoke; token is issued to client \"{}\"", requestingClientId); }

Please let us know, how can we solve it?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment