简体   繁体   中英

How to ensure only JWT tokens from own application are accepted in Spring Security secured resource server

I am using Spring Security with Spring Boot 2.2.0, trying to get Azure AD B2C working, using spring-security-oauth2-resource-server:5.2.0 and spring-security-oauth2-jose:5.2.0 .

Using this config:

@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
            .antMatchers("/api/**")
            .authenticated()
            .and()
            .oauth2ResourceServer(OAuth2ResourceServerConfigurer::jwt);
    }
}

with spring.security.oauth2.resourceserver.jwt.jwk-set-uri set in my application.properties .

I can get a token from Azure AD B2C and access my own API endpoint using that token. However, if I use a token from another directory , the endpoint can also be accessed.

I do see in the claims of the principal that this comes from another azure directory. Is this something I need to manually add in my application (testing if the application id matches in the claims)? Or should I add some other configuration that I have not done yet?

I also tried adding my own JwtDecoder bean like this using JwtDecoders.fromOidcIssuerLocation("https://mycompb2ctestorg.b2clogin.com/mycompb2ctestorg.onmicrosoft.com/v2.0/?p=B2C_1_ropc_flow"); , but that gives:

java.lang.IllegalStateException: The Issuer "https://mycompb2ctestorg.b2clogin.com/60780907-bc3a-469a-82d1-b89ffed655af/v2.0/" 
provided in the configuration did not match the requested issuer 
"https://mycompb2ctestorg.b2clogin.com/mycompb2ctestorg.onmicrosoft.com/v2.0/?p=B2C_1_ropc_flow"

Also, using:

spring.security.oauth2.resourceserver.jwt.issuer-uri=https://mycompb2ctestorg.b2clogin.com/mycompb2ctestorg.onmicrosoft.com/v2.0/?p=B2C_1_ropc_flow

gives the same exception as trying to declare my own JwtDecoder bean.

After reading about custom token validators in the Spring Security docs, I have added a custom validator that checks the audience claim to ensure the token was issued for my own application. To do this, create this validator class:

private static class AudienceValidator implements OAuth2TokenValidator<Jwt> {

    @Override
    public OAuth2TokenValidatorResult validate(Jwt token) {
        if (token.getAudience().contains("my-application-id-here")) {
            return OAuth2TokenValidatorResult.success();
        } else {
            return OAuth2TokenValidatorResult.failure(
                    new OAuth2Error("invalid_token", "The audience is not as expected, got " + token.getAudience(),
                                    null));
        }
    }
}

And use it by declaring your own JwtDecoder bean in your WebSecurityConfigurerAdapter configuration class:

@Bean
public JwtDecoder jwtDecoder() {
    NimbusJwtDecoder result = NimbusJwtDecoder.withJwkSetUri(properties.getJwt().getJwkSetUri()).build();
    result.setJwtValidator(
            new DelegatingOAuth2TokenValidator<Jwt>(
                   JwtValidators.createDefault(), 
                   new AudienceValidator()) 
            );
    return result;
}

The default validator will check things like the timestamp. If that is ok, the AudienceValidator will check the audience claim.

NOTE: The order that you pass in the validators in the DelegatingOAuth2TokenValidator defines the order of how the JWT token is checked. In the example here, the timestamp is checked before the audience. If you want the audience check first, you need put it first in the constructor of DelegatingOAuth2TokenValidator

I know I'm late to the party but there is some information missing in the current answers.

The IllegalStateException:

provided in the configuration did not match the requested issuer 

is very likely caused as @Dragana Le Mitova already mentioned by the slash at the end of the issue-uri property. This needs to be set to:

spring.security.oauth2.resourceserver.jwt.issuer-uri=https://mycompb2ctestorg.b2clogin.com/mycompb2ctestorg.onmicrosoft.com/v2.0

Spring will then pull the configuration for the OAuth Resource Server from https://mycompb2ctestorg.b2clogin.com/mycompb2ctestorg.onmicrosoft.com/v2.0/.well-known/openid-configuration automatically.


To ensure that we only accept JWTs from our Azure application we need to check the audience (aud) attribute of the JWT. For Azure Applications this is usually the same as the client/app id. As @Wim Deblauwe has already answered, this is done with custom validators for the JwtDecoder . Spring Security even gives us an example in their documentation for a custom JWT validator in which they implement the audience claim check. This is done by providing our own validator in the JwtDecoder bean.

Audience (aud) claim validator:

public class AudienceValidator implements OAuth2TokenValidator<Jwt> {
    OAuth2Error error = new OAuth2Error("invalid_token", "The required audience is missing", null);

    public OAuth2TokenValidatorResult validate(Jwt jwt) {
        if (jwt.getAudience().contains("messaging")) {
            return OAuth2TokenValidatorResult.success();
        } else {
            return OAuth2TokenValidatorResult.failure(error);
        }
    }
}

JwtDecoder:

@Bean
JwtDecoder jwtDecoder() {
    NimbusJwtDecoderJwkSupport jwtDecoder = (NimbusJwtDecoderJwkSupport)
        JwtDecoders.withOidcIssuerLocation(issuerUri);

    OAuth2TokenValidator<Jwt> audienceValidator = new AudienceValidator();
    OAuth2TokenValidator<Jwt> withIssuer = JwtValidators.createDefaultWithIssuer(issuerUri);
    OAuth2TokenValidator<Jwt> withAudience = new DelegatingOAuth2TokenValidator<>(withIssuer, audienceValidator);

    jwtDecoder.setJwtValidator(withAudience);

    return jwtDecoder;
}

This is slightly different than the answer provided by @Wim Deblauwe in the way that we do create the validators using JwtValidators.createDefaultWithIssuer(issuerUri) instead of JwtValidators.createDefault() . This is important because we essentially want to check 3 JWT attributes:

  • Expiration
  • Issuer
  • Audience

With JwtValidators.createDefault() we only create a validator for the expiration attribute. With JwtValidators.createDefaultWithIssuer(issuerUri) we create validators for the expiration and issuer attribute. JwtValidators.createDefaultWithIssuer(issuerUri) is also the default behavior of Spring Security when setting the issue-uri property.

There is an ongoing discussion if the way we currently have to add custom validators is optimal if you want to go a bit more in-depth.

I suspect that the exception you are getting when trying to declare your own JwtDecoder beam, comes from the slash that is missing.

Note that the " requested issuer " always lacks the trailing slash, even if it was explicitly specified with trailing slash in the configuration.

Spring Security removes any trailing slash from the issuer URI before appending, as mandated by the spec. However, after having fetched the configuration, the returned configuration's issuer URI matched against the "cleaned up" version of the issue URI, rather than the the originally provided one.

Because fromOidcIssuerLocation() doesn't know about the originally provided issuer URL, it matches against the cleanedIssuer, which causes the described problem. The easiest way to solve that is to do the cleanup within fromOidcIssuerLocation() , which then still has the original version available for matching.

Another note related to the first question you asked.

If I know your API's identifier + your tenant id, I can acquire an access token for your API using client credentials, The token will not contain scopes or roles. it cannot. So it is critical that you check for the presence of valid delegated permissions (aka scopes) or valid app permissions (in roles claim).

Look at this particular code.

 JwtSecurityTokenHandler tokenHandler = new JwtSecurityTokenHandler();

        TokenValidationParameters validationParameters = new TokenValidationParameters
        {
            // We accept both the App Id URI and the AppId of this service application
            ValidAudiences = new[] { audience, clientId },

            // Supports both the Azure AD V1 and V2 endpoint
            ValidIssuers = new[] { issuer, $"{issuer}/v2.0" },
            IssuerSigningKeys = signingKeys
        };

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM