Spring Security Using JWT in Spring Boot Application
We wanted to implement OAuth2 security using JWT to protect our API(s). We are running our micro-service application in kubernetes platform and using Active Directory as our Authorization Server.
High Level Flow
In our case the Authorization server is “Azure AD”. The resource server is our micro-service which exposes some API(s) which we want to protect. The client in our case is other applications and not a user. i.e. B2B.
Azure AD requires some changes to make it work as per OAuth2 compliance. you can follow this article on medium.com https://medium.com/@abhinavsonkar/making-azure-ad-oidc-compliant-5734b70c43ff to first setup your application in Azure AD and make in OAuth2 compliant.
Spring boot code change.
- You need below 3 dependencies in your project.
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-oauth2-jose</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
</dependency>
2. You need a SecurityConfiguration class which extends WebSecurityConfigurerAdapter. This class is where you would provide configuration such as which API(s) to protect and how to decode the JWT token.
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.annotation.web.configurers.oauth2.server.resource.OAuth2ResourceServerConfigurer;
import org.springframework.security.oauth2.core.DelegatingOAuth2TokenValidator;
import org.springframework.security.oauth2.core.OAuth2TokenValidator;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.security.oauth2.jwt.JwtDecoder;
import org.springframework.security.oauth2.jwt.JwtDecoders;
import org.springframework.security.oauth2.jwt.JwtValidators;
import org.springframework.security.oauth2.jwt.NimbusJwtDecoder;@EnableWebSecurity
public class OAuth2ResourceServerSecurityConfiguration extends WebSecurityConfigurerAdapter {@Value(“${spring.security.oauth2.resourceserver.jwt.jwk-set-uri}”)
private String jwkSetUri;@Value(“${spring.security.oauth2.resourceserver.jwt.issuer-uri}”)
private String jwkIssuerUri;@Autowired
JwtTokenValidator jwtTokenValidator;@Override
protected void configure(HttpSecurity http) throws Exception {
// below line is required otherwise POST call will not work
http.csrf().disable();
// (HttpMethod)null is to protect all HttpMethods
.antMatchers((HttpMethod)null, “/my/protected/api/path/uri”).authenticated();
}
catch (Exception e) {
LogUtils.logError(JavaUtils.REQ_MARKER, FAILURE_MESSAGE + e.getMessage());
LogUtils.printErrorTrace(JavaUtils.REQ_MARKER, e);
}
}
)
.oauth2ResourceServer(OAuth2ResourceServerConfigurer::jwt);
}@Bean
JwtDecoder jwtDecoder() {
NimbusJwtDecoder jwtDecoder = (NimbusJwtDecoder) JwtDecoders.fromIssuerLocation(this.jwkIssuerUri);
OAuth2TokenValidator<Jwt> withIssuer = JwtValidators.createDefaultWithIssuer(this.jwkIssuerUri);
OAuth2TokenValidator<Jwt> withAudience = new DelegatingOAuth2TokenValidator<>(withIssuer, this.jwtTokenValidator);
jwtDecoder.setJwtValidator(withAudience);
return jwtDecoder;
}}
The Spring security by default validates the “expiry” and “iss” for a token. in the above code we are additionally validating the “aud”.
3. We are using a separate class validate the JWT token and return the response accordingly by implementing OAuth2TokenValidator<Jwt>
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.oauth2.core.OAuth2Error;
import org.springframework.security.oauth2.core.OAuth2TokenValidator;
import org.springframework.security.oauth2.core.OAuth2TokenValidatorResult;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.stereotype.Component;@Component
public class JwtTokenValidator implements OAuth2TokenValidator<Jwt> {@Override
public OAuth2TokenValidatorResult validate(Jwt token) {
if (token.getAudience().contains(“<our-azure-ad-application-id>”)) {
return OAuth2TokenValidatorResult.success();
}
else {// ErrorCode class below is our custom enum class which we have created.
return OAuth2TokenValidatorResult.failure(new OAuth2Error(ErrorCode.AUTHENTICATION_FAILURE.getErrorCode()));
}
}
}
4. You need to define 2 important URL’s as part of your application.properties
spring.security.oauth2.resourceserver.jwt.jwk-set-uri and spring.security.oauth2.resourceserver.jwt.issuer-uri
The values for these URL’s can be fetched from Azure AD portal. e.g.
spring.security.oauth2.resourceserver.jwt.jwk-set-uri=https://login.microsoftonline.com/<your-org-tenant-id>/discovery/v2.0/keysspring.security.oauth2.resourceserver.jwt.issuer-uri=https://login.microsoftonline.com/<your-org-tenant-id>/v2.0
Now, if you start your server and try to call your protected API path through any REST client like postman you should get 401 unless you send the JWT token.
After making these changes our swagger documentation broke because we were not able to launch that and other issues was how to test these protect API(s). Below code changes are required to your swagger code to make it work.
import static io.swagger.models.auth.In.HEADER;import static java.util.Collections.singletonList;import static org.springframework.http.HttpHeaders.AUTHORIZATION;import java.util.ArrayList;import java.util.Arrays;import java.util.List;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;import org.springframework.http.HttpMethod;import org.springframework.web.bind.annotation.RestController;import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;import org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport;import com.google.common.base.Predicates;import springfox.documentation.builders.PathSelectors;import springfox.documentation.builders.RequestHandlerSelectors;import springfox.documentation.service.ApiKey;import springfox.documentation.service.AuthorizationScope;import springfox.documentation.service.SecurityReference;import springfox.documentation.spi.DocumentationType;import springfox.documentation.spi.service.contexts.SecurityContext;import springfox.documentation.spring.web.plugins.Docket;import springfox.documentation.swagger2.annotations.EnableSwagger2;@Configuration
@EnableSwagger2
public class SwaggerConfig extends WebMvcConfigurationSupport {@Beanpublic Docket api() {return new Docket(DocumentationType.SWAGGER_2).securitySchemes(singletonList(new ApiKey("JWT", AUTHORIZATION, HEADER.name()))).securityContexts(securityContext()).select().apis(RequestHandlerSelectors.withClassAnnotation(RestController.class)).build();}private List<SecurityReference> securityReference = singletonList(SecurityReference.builder().reference("JWT").scopes(new AuthorizationScope[0]).build());private List<HttpMethod> methods = Arrays.asList(HttpMethod.POST, HttpMethod.PUT, HttpMethod.DELETE, HttpMethod.GET);private List<SecurityContext> securityContext() {List<SecurityContext> lsc = new ArrayList<>();lsc.add(SecurityContext.builder().securityReferences(securityReference).forPaths(PathSelectors.ant("/our/protect/api/uri")).forHttpMethods(Predicates.in(methods)).build());return lsc;}@Overridepublic void addResourceHandlers(ResourceHandlerRegistry registry) {registry.addResourceHandler("swagger-ui.html").addResourceLocations("classpath:/META-INF/resources/");registry.addResourceHandler("/webjars/**").addResourceLocations("classpath:/META-INF/resources/webjars/");}}
Reference Links