Skip to content

oauth2ResourceServer JWT revocation built-in support #10558

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
ah1508 opened this issue Nov 29, 2021 · 6 comments
Closed

oauth2ResourceServer JWT revocation built-in support #10558

ah1508 opened this issue Nov 29, 2021 · 6 comments
Assignees
Labels
in: oauth2 An issue in OAuth2 modules (oauth2-core, oauth2-client, oauth2-resource-server, oauth2-jose) status: declined A suggestion or change that we don't feel we should currently apply type: enhancement A general enhancement

Comments

@ah1508
Copy link

ah1508 commented Nov 29, 2021

Dear all,

JWT revocation can be implemented manually with a publish/subscribe : a revocation event is sent (payload is the token to revoke) and subscribers (API that may receive this token and must reject it) receive this event so they can keep an up to date revocation list. Elements are automatically removed when exp is before Instant.now()

A built-in support for publish/subscribe could be a part of the in-development spring oidc server.

But on the spring security side, as far as I know it must be coded manually. Example (TTL omitted) :

Set<String> revoked = new HashSet<>();
	
@Bean
RouterFunction<ServerResponse> revocations(){
	return RouterFunctions.route(RequestPredicates.POST("/revocations"), req -> {
		String token = req.body(String.class); // content-type is text/plain
		revoked.add(token);
		return ServerResponse.noContent().build();
	});
}
	
@Bean
WebSecurityConfigurerAdapter securityConfigurerAdapter(JwtDecoder jwtDecoder /*created by auto-configuration*/) {
	return new WebSecurityConfigurerAdapter() {
		@Override
		protected void configure(HttpSecurity http) throws Exception {
			http.oauth2ResourceServer(cust -> {
				cust.jwt(jwtCust -> {
					jwtCust.decoder(token -> {
						if(revoked.contains(token)) {
							throw new SecurityException();
						}
						return jwtDecoder.decode(token);
					});
				});
			});
			http.csrf().disable();
		}
	};
}

How about a built-in support in spring Security ?

If the revocation endpoint and the revocations set are declared manually the configuration would looks like :

Set<String> revoked = new HashSet<>();

@Bean
RouterFunction<ServerResponse> revocations() {
	return RouterFunctions.route(RequestPredicates.POST("/revocations"), req -> {
		String token = req.body(String.class);
		revoked.add(token);
		return ServerResponse.noContent().build();
	});
}

// omitted : scheduled removal of expired tokens

@Bean
WebSecurityConfigurerAdapter securityConfigurerAdapter(JwtDecoder jwtDecoder){
	return new WebSecurityConfigurerAdapter() {
		@Override
		protected void configure(HttpSecurity http) throws Exception {
			http.oauth2ResourceServer(cust -> {
				cust.jwt(jwtCust -> {
					jwtCust.setRevocations(this.revoked);
				});
			});
			http.csrf().disable();
		}
	};
}

If the revocation endpoint is built-in, like an actuator :

@Bean
WebSecurityConfigurerAdapter securityConfigurerAdapter(){
	return new WebSecurityConfigurerAdapter() {
		@Override
		protected void configure(HttpSecurity http) throws Exception {
			http.oauth2ResourceServer(cust -> {
				cust.jwt(jwtCust -> {
					jwtCust.enableRevocations();
				});
			});
			http.csrf().disable();
		}
	};
}

With a customizer :

cust.jwt(jwtCust -> {
	jwtCust.enableRevocations(revocationCustomizer -> {
		revocationCustomizer.rejectIf(token -> /*code that return true or false*/);
		revocationCustomizer.rejectResponseHandler(token -> ResponseEntity.status(401).header("WWW-Authenticate", "Bearer error=\"invalid_token\", error_description=\"An error occurred while attempting to decode the Jwt: The token is revoked!\", error_uri=\"https://tools.ietf.org/html/rfc6750#section-3.1\"").build());
        });
});

and configuration properties would allow to configure revocation endpoint path and RBAC for this endpoint (exemple : hasRole('admin')).

@ah1508 ah1508 added status: waiting-for-triage An issue we've not yet triaged type: enhancement A general enhancement labels Nov 29, 2021
@sjohnr sjohnr self-assigned this Nov 29, 2021
@sjohnr sjohnr added in: oauth2 An issue in OAuth2 modules (oauth2-core, oauth2-client, oauth2-resource-server, oauth2-jose) and removed status: waiting-for-triage An issue we've not yet triaged labels Nov 29, 2021
@sjohnr
Copy link
Contributor

sjohnr commented Nov 29, 2021

Hi @ah1508, thanks for the detailed suggestion.

I could see something like this being useful for folks, but I have a couple of thoughts for discussion.

First, are there any specifications that could be used to guide the feature development in the right direction? It would be very helpful to the process if you've found and could present specifics from any RFCs that address this.

Second, it would be nice if this enhancement could be narrowed down to a specific starting point, perhaps introducing a JwtDecoder implementation that wraps another with the capability to delegate revocation decisions to another component. Of course, that's just one possible idea. See gh-9424 for discussion on the overall theme this feature would be involved in.

@sjohnr sjohnr added the status: waiting-for-feedback We need additional information before we can continue label Nov 29, 2021
@ah1508
Copy link
Author

ah1508 commented Nov 29, 2021

More readable version of what can be done today :

@Bean
WebSecurityConfigurerAdapter securityConfigurerAdapter(){
	return new WebSecurityConfigurerAdapter() {
		@Override
		protected void configure(HttpSecurity http) throws Exception {
			http.oauth2ResourceServer(cust -> {
				cust.jwt();
			});
			http.csrf().disable();
		}
	};
}

private Set<String> revoked = new HashSet<>();

// omitted : revocation endpoint, scheduled removal of expired tokens

@Bean
JwtDecoder jwtDecoder(@Value("${spring.security.oauth2.resourceserver.jwt.issuer-uri}") String issuer) {
	OAuth2TokenValidator<Jwt> issuerValidator = JwtValidators.createDefaultWithIssuer(issuer);
	// audience validator omitted here	
	OAuth2TokenValidator<Jwt> revocationValidator = jwt -> {
		if(this.revoked.contains(jwt.getTokenValue())) {
			var error = new OAuth2Error(OAuth2ErrorCodes.INVALID_TOKEN, "The token is revoked", "https://tools.ietf.org/html/rfc6750#section-3.1");				
			return  OAuth2TokenValidatorResult.failure(error);
		}
		return OAuth2TokenValidatorResult.success();
	};
	
	OAuth2TokenValidator<Jwt> validator = new DelegatingOAuth2TokenValidator<>(issuerValidator, revocationValidator /*+ audience validator*/);
	NimbusJwtDecoder jwtDecoder = (NimbusJwtDecoder) JwtDecoders.fromOidcIssuerLocation(issuer);
	jwtDecoder.setJwtValidator(validator);
	return jwtDecoder;
}

@spring-projects-issues spring-projects-issues added status: feedback-provided Feedback has been provided and removed status: waiting-for-feedback We need additional information before we can continue labels Nov 29, 2021
@ah1508
Copy link
Author

ah1508 commented Nov 29, 2021

@sjohnr : I missed your previous post.

https://datatracker.ietf.org/doc/html/rfc6750#section-3.1 mentions "revoked token" so the response if token is revoked can be standard (401 with WWW-Authenticated header, I updated my first post).

But revocation propagation from OIDC server to APIs that trust tokens issued by the OIDC server is not covered (or maybe I missed something). I see at least two points : self registration of the revocation endpoint by the API on the OIDC server, structure of the payload when this endpoint is called. In my example the payload is just a raw string.

This article https://auth0.com/blog/denylist-json-web-token-api-keys/ covers JWT deny list.

Without spec I cannot see how self registration can be implemented, but at least spring security could be "passive" : expose a endpoint and it is the developer responsibility to :

  • register the http endpoint as a listener on a topic
  • create a UI that shows tokens recently created for a specific user (easier to do if we control the OIDC server) and a "revoke" button that publish the "revocation event" on the topic.

The token extraction from the payload is still and issue, a json path could be given by the developer and if a standard appears latter Spring Security could declare a default value for this json path.

@sjohnr
Copy link
Contributor

sjohnr commented Nov 30, 2021

Thanks for the explanation @ah1508. I agree that without a spec, the issues you outlined are problematic for an implementation.

Yet I further see a problem with even defining such an endpoint to be provided by Spring Security, with no spec or standard guiding how exactly it is protected, what the payload is, how it works, etc. In essence, it seems that it would be almost entirely custom code and configuration holding it all together, so I'm not sure what benefit there is for Spring Security to provide it.

I could see value in the framework providing an implementation that handles some of the boilerplate, such as delegating to a specific component for making the revocation decision, possibly raising a particular type of error when a token is used after being revoked, perhaps making it easier to include that in JWT validation (as in your code sample above), etc.

Beyond that, I wonder if anyone else in the community feels that it would be extremely beneficial for the framework to provide the endpoint. Can you point to other vendors or frameworks providing such an endpoint on the resource server side?

@ah1508
Copy link
Author

ah1508 commented Nov 30, 2021

Even if is a common use case, it seems to be ignored, for instance neither quarkus or asp.net have built-in support for that.

It is a security hole, if I am notified that my account is used by someone else, all the tokens must be revoked as soon as I change my password. These tokens must be denied by all APIs that thrust the token issuer. A few minutes is enough to do a lot of damages.

About the lack of spec : indeed it is problematic, but even with a spec the RBAC for this endpoint would be configurable.

Something like

cust.jwt(jwtCust -> {
	jwtCust.enableRevocations(revocationCustomizer -> {
		revocationCustomizer.access("hasRole('admin')");
	});
});

Then it would be developer's responsability to configure the OIDC server so the endpoint can be called with the appropriate roles. Maybe it could be done during the subscription, which would mean "oidc server, when you call my revocation endpoint your request must use a bearer token that carries these roles". Fortunatelly Spring security would just have to check if the token used to call the revocation endpoint carries the required roles.

For the paylod structure : without spec it must indeed be configured by the developper :

cust.jwt(jwtCust -> {
	jwtCust.enableRevocations(revocationCustomizer -> {
        	revocationCustomizer.access("hasRole('admin')");
	        revocationCustomizer.tokenPathInPayload("$.value");
    });
});

but if a spec appears the setting would become deprecated.

@sjohnr sjohnr removed the status: feedback-provided Feedback has been provided label Nov 30, 2021
@sjohnr
Copy link
Contributor

sjohnr commented Jun 10, 2022

Hi @ah1508, I'm just circling back to some older issues and recalled our conversation above. Your conversation and feedback is really helpful and appreciated! However, I'm not seeing anything specific that we can address with this issue as it stands currently largely due to the lack of a specification that addresses it, though there are a lot of ideas that could be used for more specific suggestions. I'm going to close the issue with that in mind, but we can continue to discuss this as long as you want and if anything exciting comes up we can either re-open or open a new more specific issue.

@sjohnr sjohnr closed this as completed Jun 10, 2022
@sjohnr sjohnr added the status: declined A suggestion or change that we don't feel we should currently apply label Jun 10, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
in: oauth2 An issue in OAuth2 modules (oauth2-core, oauth2-client, oauth2-resource-server, oauth2-jose) status: declined A suggestion or change that we don't feel we should currently apply type: enhancement A general enhancement
Projects
None yet
Development

No branches or pull requests

3 participants