-
Notifications
You must be signed in to change notification settings - Fork 5.9k
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
Jwt Principal customization #16231
Comments
@vaa25, thanks for the suggestion. Before moving forward, I want to make sure we are on the same page. It sounds like you have a custom principal so that you don't need to access the claims map directly. However, Have you already tried using |
@jzheaux thanks for answer. Using Jwt directly (instead of MyPrincipal) and doing @PreAuthorize("principal.id == '100'") can work if id was stored in "jti" claim. But my point is not "id" itself. Claim can be "userId", or "accountId", or any other private Jwt claim, or part of claim or expression , e.g: @Getter
public class JwtPrincipal implements MyPrincipal {
private final Integer userId;
private final Integer accountId;
private final String country;
private final String emailDomain;
public JwtPrincipal(Jwt jwt) {
this.userId = Integer.parseInt(jwt.getClaim("userId"));
this.accountId = Integer.parseInt(jwt.getClaim("accountId"));
this.country = jwt.getClaimAsString("country");
this.emailDomain = jwt.getClaimAsString("email").split("@")[1];
}
} If some project has already implemented its own custom type of authorization with its own custom principal object ( Main purpose is to add ability to break tight coupling of controllers and Jwt authorization easy and intuitive way. |
Gotcha. It sounds like you have some custom claims and you'd like to be able to access them without going to the claims map. And it's certainly a reasonable thing to want to have a custom principal. It also sounds like you are wanting to avoid creating a custom authentication object. I think a principal converter is a bit more fine-grained than we want to get to achieve something like that, but would something like the following work?
@Component
public JwtPrincipalAuthenticationConverter implements Converter<Jwt, BearerTokenAuthentication> {
private final JwtAuthenticationConverter delegate = new JwtAuthenticationConverter();
@Override
public BearerTokenAuthentication convert(Jwt jwt) {
JwtAuthenticationToken token = this.delegate.convert(jwt);
JwtPrincipal principal = new JwtPrincipal(jwt, token.getAuthorities());
OAuth2AccessToken access = new OAuth2AccessToken(TokenType.BEARER, jwt.getTokenValue(),
jwt.getIssuedAt(), jwt.getExpiresAt());
return new BearerTokenAuthentication(principal, jwt, token.getAuthorities());
}
private static class JwtPrincipal implements MyPrincipal, OAuth2AuthenticatedPrincipal {
// ...
}
} The primary change to your design would be to have Then |
To support @Component
public class JwtPrincipalAuthenticationConverter implements Converter<Jwt, BearerTokenAuthentication> {
private final JwtAuthenticationConverter delegate = new JwtAuthenticationConverter();
public JwtPrincipalAuthenticationConverter(OAuth2ResourceServerProperties properties) {
final var jwt = properties.getJwt();
if (StringUtils.hasText(jwt.getPrincipalClaimName())){
delegate.setPrincipalClaimName(jwt.getPrincipalClaimName());
}
final var grantedAuthoritiesConverter = new JwtGrantedAuthoritiesConverter();
if (StringUtils.hasText(jwt.getAuthoritiesClaimName())){
grantedAuthoritiesConverter.setAuthoritiesClaimName(jwt.getAuthoritiesClaimName());
}
if (StringUtils.hasText(jwt.getAuthoritiesClaimDelimiter())){
grantedAuthoritiesConverter.setAuthoritiesClaimDelimiter(jwt.getAuthoritiesClaimDelimiter());
}
if (jwt.getAuthorityPrefix() != null){
grantedAuthoritiesConverter.setAuthorityPrefix(jwt.getAuthorityPrefix());
}
delegate.setJwtGrantedAuthoritiesConverter(grantedAuthoritiesConverter);
}
@Override
public BearerTokenAuthentication convert(Jwt jwt) {
JwtAuthenticationToken token = (JwtAuthenticationToken)this.delegate.convert(jwt);
JwtPrincipal principal = new JwtPrincipal(jwt, token.getAuthorities());
OAuth2AccessToken access = new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER, jwt.getTokenValue(),
jwt.getIssuedAt(), jwt.getExpiresAt());
return new BearerTokenAuthentication(principal, access, token.getAuthorities());
}
private static class JwtPrincipal implements MyPrincipal, OAuth2AuthenticatedPrincipal {
private final Jwt jwt;
private final Collection<GrantedAuthority> grantedAuthorities;
public JwtPrincipal(Jwt jwt, Collection<GrantedAuthority> authorities) {
this.jwt = jwt;
this.grantedAuthorities = authorities;
}
@Override
public Integer getId() {
return Integer.parseInt(jwt.getClaim("id"));
}
@JsonIgnore
@Override
public Map<String, Object> getAttributes() {
return jwt.getClaims();
}
@JsonIgnore
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return grantedAuthorities;
}
@JsonIgnore
@Override
public String getName() {
return jwt.getSubject();
}
}
} To make it work we have to apply JwtPrincipalAuthenticationConverter via configurer, because it does't work as a bean. The JwtAuthenticationConverter can be picked up as a bean only. @Bean
public SecurityFilterChain filterChain(HttpSecurity http, JwtPrincipalAuthenticationConverter jwtPrincipalAuthenticationConverter) throws Exception {
return http
.oauth2ResourceServer(x -> x.jwt(jwtConfigurer -> jwtConfigurer.jwtAuthenticationConverter(jwtPrincipalAuthenticationConverter)))
.build();
} In general, this solution satisfies all my functional criteria, but implementation could be much simpler and intuitive ;) Just to compare: @Component
public class MyJwtPrincipalConverter implements JwtPrincipalConverter {
@Override
public Object convert(Jwt jwt, String principalName) {
return new JwtPrincipal(jwt);
}
private static class JwtPrincipal implements MyPrincipal {
private final Jwt jwt;
private JwtPrincipal(Jwt jwt) {
this.jwt = jwt;
}
@Override
public Integer getId() {
return Integer.parseInt(jwt.getClaim("id"));
}
}
} Thank you! |
Thanks for the extra detail, @vaa25. To make sure we are comparing apples to apples, let's see if I can help reduce some of that boilerplate first. Because Spring Boot publishes a @Component
public class JwtPrincipalAuthenticationConverter implements Converter<Jwt, JwtAuthenticationToken> {
private final JwtAuthenticationConverter delegate;
public JwtPrincipalAuthenticationConverter(JwtAuthenticationConverter delegate) {
this.delegate = delegate;
}
@Override
public BearerTokenAuthentication convert(Jwt jwt) {
AbstractAuthenticationToken token = this.delegate.convert(jwt);
JwtPrincipal principal = new JwtPrincipal(jwt);
OAuth2AccessToken access = new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER, jwt.getTokenValue(),
jwt.getIssuedAt(), jwt.getExpiresAt());
return new BearerTokenAuthentication(principal, access, token.getAuthorities());
}
private static class JwtPrincipal implements MyPrincipal, OAuth2AuthenticatedPrincipal {
private final Jwt jwt;
JwtPrincipal(Jwt jwt) {
this.jwt = jwt;
}
@Override
public Integer getId() {
return Integer.parseInt(jwt.getClaim("id"));
}
@JsonIgnore
@Override
public Map<String, Object> getAttributes() {
return jwt.getClaims();
}
@JsonIgnore
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return Collections.empty();
}
@JsonIgnore
@Override
public String getName() {
return jwt.getSubject();
}
}
} And then as you said: @Bean
public SecurityFilterChain filterChain(HttpSecurity http, JwtPrincipalAuthenticationConverter jwtPrincipalAuthenticationConverter) throws Exception {
return http
.oauth2ResourceServer(x -> x.jwt(jwtConfigurer -> jwtConfigurer.jwtAuthenticationConverter(jwtPrincipalAuthenticationConverter)))
.build();
} Note that some of what you propose is also not quite realistic; for example, Security can't support a converter API that returns an Given that, I think we can get close to what you are proposing. For example, we can enhance @Component
public class MyJwtPrincipalConverter implements Converter<Jwt, OAuth2AuthenticatedPrincipal> {
@Override
public OAuth2AuthenticatedPrincipal convert(Jwt jwt) {
return new JwtPrincipal(jwt);
}
private static class JwtPrincipal implements MyPrincipal, OAuth2AuthenticatedPrincipal {
private final Jwt jwt;
private JwtPrincipal(Jwt jwt) {
this.jwt = jwt;
}
@Override
public Integer getId() {
return Integer.parseInt(jwt.getClaim("id"));
}
// ...
}
}
// ...
@Bean
JwtBearerTokenAuthenticationConverter authenticationConverter(JwtAuthenticationConverter fromBoot,
MyJwtPrincipalConverter principal) {
Converter<Jwt, Collection<GrantedAuthority> authorities = (jwt) -> fromBoot.convert(jwt).getAuthorities();
JwtBearerTokenAuthenticationConverter authentication = new JwtBearerTokenAuthenticationConverter();
authentication.setJwtAuthenticatedPrincipalConverter(principal);
authentication.setJwtGrantedAuthoritiesConverter(authorities);
return authentication;
} Is this a step in the right direction? If so, we can get this scheduled for the next minor release. |
To inject JwtAuthenticationConverter is a great idea, thank you! As for JwtBearerTokenAuthenticationConverter modification - developer still have to add some additional code: public class Config {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http, JwtBearerTokenAuthenticationConverter jwtPrincipalAuthenticationConverter) throws Exception {
return http
.oauth2ResourceServer(x -> x.jwt(jwtConfigurer -> jwtConfigurer.jwtAuthenticationConverter(jwtPrincipalAuthenticationConverter)))
.build();
}
@Bean
public JwtBearerTokenAuthenticationConverter authenticationConverter(JwtAuthenticationConverter fromBoot, MyJwtPrincipalConverter myJwtPrincipalConverter) {
Converter<Jwt, Collection<GrantedAuthority>> authoritiesConverter = jwt -> fromBoot.convert(jwt).getAuthorities();
JwtBearerTokenAuthenticationConverter authentication = new JwtBearerTokenAuthenticationConverter();
authentication.setJwtAuthenticatedPrincipalConverter(myJwtPrincipalConverter);
authentication.setJwtGrantedAuthoritiesConverter(authoritiesConverter);
return authentication;
}
}
@Component
public class MyJwtPrincipalConverter implements Converter<Jwt, OAuth2AuthenticatedPrincipal> {
private final JwtAuthenticationConverter delegate;
public MyJwtPrincipalConverter(JwtAuthenticationConverter delegate) {
this.delegate = delegate;
}
@Override
public OAuth2AuthenticatedPrincipal convert(Jwt jwt) {
final var authenticationToken = delegate.convert(jwt);
return new JwtPrincipal(jwt, authenticationToken.getName(), authenticationToken.getAuthorities());
}
private static class JwtPrincipal implements MyPrincipal, OAuth2AuthenticatedPrincipal {
private final Jwt jwt;
private final String name;
private final Collection<? extends GrantedAuthority> authorities;
private JwtPrincipal(Jwt jwt, String name, Collection<GrantedAuthority> authorities) {
this.jwt = jwt;
this.name = name;
this.authorities = authorities;
}
@Override
public Integer getId() {
return Integer.parseInt(jwt.getClaim("id"));
}
@JsonIgnore
@Override
public Map<String, Object> getAttributes() {
return jwt.getClaims();
}
@JsonIgnore
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return authorities;
}
@JsonIgnore
@Override
public String getName() {
return name;
}
}
} My idea requires from developer just to implement JwtPrincipalConverter as a bean. See spring-security code change here |
I believe what the developer would have to do is as I described; one top-level class and one
In sum, I don't see how to change Security's code to do what you proposed (e.g. the |
JwtPrincipalConverter is not same as Converter<Jwt, OAuth2AuthenticatedPrincipal> public interface JwtPrincipalConverter {
Object convert(Jwt jwt, String principalName);
} You can see my implementation of Security's code change here main...vaa25:spring-security:gh-16231-my-idea |
I tried commenting on the code, but GH might not support that on commits. Want to submit a PR and we can discuss? |
Thanks! I've left my comments there. Let's continue from that point. I'm going to close this ticket in favor of #16311 |
Expected Behavior
I want to use method authorization annotations and @AuthenticationPrincipal clean way with custom business fields using oauth2 authorization with spring-boot-starter-oauth2-resource-server supporting all spring.security.oauth2.resourceserver.jwt.* properties.
response:
It can be achieved if Authentication object will have custom principal object:
I'd like to add a principal converter as a bean or via OAuth2ResourceServerConfigurer for customization
Current Behavior
Current implementation sets org.springframework.security.oauth2.jwt.Jwt object as principal causing such implementation:
Here I have to write 'claims' in @PreAuthorize that is not business field, and receive Jwt principal as method parameter that is not business object.
Context
I believe having custom Principal object makes authorization control more readable and simple and can help to accept requests with different types of authorization into same controller method.
There is some workarounds, but they are not so simple as my proposition and may require third-party library.
Based on stackoverflow question
P.S. I have implemented this feature locally, just waiting for your approve of this idea, guys.
The text was updated successfully, but these errors were encountered: