Hi Spring-Security experts,

I have a feature request which will help error handling on NimbusJwtDecoder.

Expected Behavior

NimbusJwtDecoder#decode should differentiate its exceptions so that library consumers can do proper checks.

I am proposing a new enum constant class for clear reason for the errors.

It will be something like the below class:

public enum JwtExceptionConstants {

    EXPIRED_JWT("Expired JWT"),
    SIGNATURE_VALIDATION("Signature Validation has failed"),
    ...
    CUSTOM("Custom error");

    private final String message;

    private JwtExceptionConstants(String message) {
        this.message = message;
    }

    public String getMessage() {
        return this.message;
    }

}

The above class can be added to the JwtException instance variable to indicate what is the reason for the JwtException being raised.

My desired pseudo code looks like:

// setup decoder1
DefaultJWTProcessor<SecurityContext> jwtProcessor = new DefaultJWTProcessor<>();
jwtProcessor.setJWTClaimsSetVerifier(new DefaultJWTClaimsVerifier<SecurityContext>() {
    @Override
    public void verify(JWTClaimsSet claimsSet, SecurityContext securityContext) throws BadJWTException {
        super.verify(claimsSet, securityContext);
        String issuer = claimsSet.getIssuer();
        String trustIssuer = "fake-issuer.com/token";
        if (Strings.isNullOrEmpty(issuer) || !issuer.equals(trustIssuer)) {
            throw new JwtException("msg", JwtExceptionConstants.CUSTOM);
        }
    }
});
JwtDecoder decoder1 = new NimbusJwtDecoder(jwtProcessor);

// setup decoder2
// ...
JwtDecoder decoder2 = new NimbusJwtDecoder();

Authenticator1 authenticator1 = new Authenticator1(decoder1);
Authenticator2 authenticator2 = new Authenticator2(decoder2);

public HttpResponse canAuthenticate(String token) {
    try {
        decoder1.decode(token)
    } catch (JwtException e) {
        if (e.getFailureReason() == JwtExceptionConstants.CUSTOM) {
            authenticator1.authenticate(token);
        } else {
            authenticator2.authenticate(token);
        }
    }
}

It should have common validation errors as constant fields in the enum and a custom field as well.

Current Behavior

Currently, JwtException is thrown with error messages when trying to decode JWT using NimbusJwtDecoder.

The example pseudo code is following:

JwtDecoder decoder1 = new NimbusJwtDecoder();
// setup decoder1
JwtDecoder decoder2 = new NimbusJwtDecoder();
// setup decoder2

Authenticator1 authenticator1 = new Authenticator1(decoder1);
Authenticator2 authenticator2 = new Authenticator2(decoder2);

public HttpResponse canAuthenticate(String token) {
    try {
        decoder1.decode(token)
    } catch (JwtException e) {
        if (e.getMessage().contains("msg")) {
            authenticator1.authenticate(token);
        } else {
            authenticator2.authenticate(token);
        }
    }
}

Currently, there is no fixed way to distinguish exceptions thrown from the NimbusJwtDecoder#decode method other than looking for an error message. This can be potentially dangerous as it needs all components be synced up, which requires a lot of effort in big size applications. By having constants, it's much easier to perform different operations based on the exception reasons, without having to check what message is being returned.

Context

The code snippet I posted in the Current Behaviour section is a workaround I have.

Comment From: jzheaux

@edmundham, thanks for the suggestion. This is also something that I raised with Nimbus a while back - I think that would probably need to come first since there are many times when Spring Security wouldn't know what code to add.

Perhaps you'd be interested in engaging with Nimbus to supply a PR over there?

In the meantime, if I'm reading your pseudocode correctly, I'd guess that subclassing JwtException would work for you.

For example, could you do:

DefaultJWTProcessor<SecurityContext> jwtProcessor = new DefaultJWTProcessor<>();
jwtProcessor.setJWTClaimsSetVerifier(new DefaultJWTClaimsVerifier<SecurityContext>() {
    @Override
    public void verify(JWTClaimsSet claimsSet, SecurityContext securityContext) throws BadJWTException {
        super.verify(claimsSet, securityContext);
        String issuer = claimsSet.getIssuer();
        String trustIssuer = "fake-issuer.com/token";
        if (Strings.isNullOrEmpty(issuer) || !issuer.equals(trustIssuer)) {
            throw new CustomJwtException("msg");
        }
    }
});

// ... 

public HttpResponse canAuthenticate(String token) {
    try {
        decoder1.decode(token)
    } catch (CustomJwtException e) {
        authenticator1.authenticate(token);
    } catch (JwtException e) {
        authenticator2.authenticate(token);
    }
}

Let me know if I've misread your use case.

As a side note, I wonder if you've already taken a look at JwtIssuerValidator, which is designed to ensure that the token's issuer matches a given value.

Also, it seems you might be doing some form of multi-tenancy (selecting different authentication strategies based on request material), which makes me wonder if you've seen JwtIssuerAuthenticationManagerResolver.

Comment From: edmundham

Hi @jzheaux , thanks for the suggestion. I will take a look at JwtIssuerValidator and JwtIssuerAuthenticationManagerResolver.

There is another case where I need to check exceptions thrown by one of the default validators (expiry check), but it does seem like an enhancement on Nimbus side. Closing this ticket :)