This project provides several standalone modules supporting common security token handling in Java and Kotlin applications, e.g. either as an OAuth 2.0 client or a OAuth 2.0 Resource Server with JWT validation.
The main motivation behind the project is to support multiple token issuers/identity providers with as much ease as possible.
The modules are roughly split in two:
- token-client-* (support for multiple OAuth 2.0 grants/flows as a client)
- token-validation-* (support for JWT token validation following best practice and OAuth 2.0 specifications)
The client and validation modules provide a core module and specific "wrapper" implementations for some well known libraries such as Spring Boot, JAX-RS and KTOR.
This project is mainly composed of modules wrapping other well known libraries. Currently there is support for:
- Spring Boot and Spring Web MVC
- JAX-RS
- Plain Java
- Kotlin with ktor
Provides the core token validation support, using the Nimbus OAuth 2.0 SDK with OpenID Connect extensions. Token signing keys are retrieved from the external issuer and cached in the JwtTokenValidator
, using the com.nimbusds.openid.connect.sdk.validators.IDTokenValidator
(this validator also works well for validating access_tokens). Token signing keys will be fetched from the jwt_keys endpoint configured in the provider configuration metadata endpoint (e.g. /.well-known/openid-configuration
) when required (e.g. new signing keys are detected).
This module can be used standalone (e.g. if you do not use Spring or JAX-RS). You will however need to code the part to "hook in" enforcement and trigger the token validation (e.g. as done in the token-validation-filter module). If you use Spring, JAX-RS or Ktor you should use the specific module for your framework.
Simple servlet filter using the token-validation-core components for validating tokens on inbound HTTP requests. Can be used standalone but is more commonly used together with Spring or JAX-RS.
Spring Boot (Spring Web) specific wrapper around token-validation-core and token-validation-filter providing auto configuration of the relevant components.
To enable token validation for a spring boot application, simply annotate your SpringApplication class or any Spring Configuration class with @EnableJwtTokenValidation
. Optionally list the packages or classses you dont want token validations for (e.g. error controllers). The package org.springframework
- e.g. @EnableJwtTokenValidation(ignore="org.springframework")
is listed as ignored by default if you dont specify a ignore list. Use the @Unprotected
or @Protected
/@ProtectedWithClaims
annotations at rest controller method/class level to indicate if token validation is required or not.
There is a short sample below, however more detailed samples are available in the token-validation-spring-demo
module.
This annotation will enable token validation and token transportation/propagation through the service
@SpringBootApplication
@EnableJwtTokenValidation
public class ProductServiceApplication {
public static void main(String[] args) {
SpringApplication.run(ProductServiceApplication.class, args);
}
}
This example shows
- First method - An unprotected endpoint. No token is required to use this endpoint.
- Second method - A protected endpoint. This endpoint will require a valid token from the "employee" issuer.
- Third method - A protected endpoint. This endpoint will require a valid token from one of the configured issuers.
- Fourth method - A non-annotated endpoint. This endpoint will not be accessible from outside the server (will return a 501 NOT_IMPLEMENTED).
@RestController
class ProductController {
private final ProductService productService;
ProductController(ProductService productService){
this.productService = productService;
}
@Unprotected
@RequestMapping(value = "/product/{id}", method = RequestMethod.GET)
public Product read(@PathVariable("id") String id) {
return productService.read(id);
}
@Protected(issuer="employee")
@RequestMapping(value = "/product", method = RequestMethod.POST)
public Product create(@RequestBody Product product) {
return productService.create(product);
}
@Protected
@RequestMapping(value = "/product/sample", method = RequestMethod.GET)
public Product sample() {
Product product = Product.sample();
product.setId(UUID.randomUUID().toString());
return product;
}
@RequestMapping(value = "/product/{id}/variant", method = RequestMethod.GET)
public Variant readVariant(@PathVariable("id") String id) {
return productService.readVariant(id);
}
}
JAX-RS wrapper around token-validation-core and token-validation-filter. Two steps are necessary to enable token validation:
- Register the servlet filter
JaxrsJwtTokenValidationFilter
with your servlet container to pick up the token of incoming requests - Register the
JwtTokenContainerRequestFilter
with yourApplication
(plain JAX-RS) orResourceConfig
(Jersey)
Use the @Unprotected
, @Protected
or @ProtectedWithClaims
annotations on resource methods and/or classes indicate if token validation is required or not. Method annotations have precedence
One additional step is required to pass the token do other services
- Register
JwtTokenClientRequestFilter
with your client
Register the servlet filter with your container, as done in spring boots @SpringBootConfiguration
in the snippet below. How you get hold of the configuration map of issuers is up to the implementor.
@Bean
public FilterRegistrationBean<JwtTokenValidationFilter> jwtTokenValidationFilterBean(MultiIssuerConfiguraton config) {
return new FilterRegistrationBean<>(new JaxrsJwtTokenValidationFilter(config));
}
@Bean
public MultiIssuerConfiguraton multiIssuerConfiguration(Map<String, IssuerProperties> issuerConfiguration, ProxyAwareResourceRetriever resourceRetriever) {
return new MultiIssuerConfiguraton(issuerConfiguration, resourceRetriever);
}
@Bean
public ProxyAwareResourceRetriever proxyAwareResourceRetriever() {
return new ProxyAwareResourceRetriever();
}
How you configure the servlet filter depends on how you launch your app, e.g. if you use spring or not, and whether you use tomcat or jetty
There are two options, explicit registering of resources
new ResourceConfig()
.register(JwtTokenContainerRequestFilter.class)
// ...
.register(Resource.class);
or let jersey scan for Resources and/or @Provider
-annotated classes
new ResourceConfig()
.packages("no.nav.security.token.support.jaxrs", "x.y.z.resourcepackage");
This example shows
- First method - An unprotected endpoint. No token is required to use this endpoint.
- Second method - A protected endpoint. This endpoint will require a valid token from one of the configured issuers.
- Third method - A protected endpoint. This endpoint will require a valid token from the "employee" or "manager" issuer.
- Fourth method - A protected endpoint. This endpoint will require a valid token from the "manager" issuer and a claim where key is "acr" and value is "Level4".
- Fifth method - A non-annotated endpoint. This endpoint will not be accessible from outside the server (will return a 501 NOT_IMPLEMENTED).
@Path("/rest")
public class ProductResource {
// ...
@GET
@PATH("/product/")
@Unprotected
public List<Product> list() {
return service.list();
}
@POST
@PATH("/product")
@Protected
public Product add(Product product) {
return service.create(product);
}
@PUT
@PATH("/product")
@RequiredIssuers(value = {
ProtectedWithClaims(issuer = "employee"),
ProtectedWithClaims(issuer = "manager")
})
public Product add(Product product) {
return service.update(product);
}
@DELETE
@PATH("/product/{id}")
@ProtectedWithClaims(issuer = "manager", claimMap = { "acr=Level4" })
public void add(String id) {
return service.delete(id);
}
@GET
@PATH("/product/{id}")
public void add(String id) {
return service.get(id);
}
}
The claimMap in @ProtectedWithClaims
can contain entries where the expected value is an asterisk, e.g.: "acr=*"
. This will require that the claim is present in the token, without regards to its value.
See demo application in token-validation-ktor-demo
for example configurations and setups.
Add the modules that you need as Maven dependencies.
- token-validation-spring:
<dependency>
<groupId>no.nav.security</groupId>
<artifactId>token-validation-spring</artifactId>
<version>${token-support.version}</version>
</dependency>
- token-validation-jaxrs:
<dependency>
<groupId>no.nav.security</groupId>
<artifactId>token-validation-jaxrs</artifactId>
<version>${token-support.version}</version>
</dependency>
- token-validation-ktor:
<dependency>
<groupId>no.nav.security</groupId>
<artifactId>token-validation-ktor</artifactId>
<version>${token-support.version}</version>
</dependency>
- token-validation-core (included as dependency in all of the above):
<dependency>
<groupId>no.nav.security</groupId>
<artifactId>token-validation-core</artifactId>
<version>${token-support.version}</version>
</dependency>
no.nav.security.jwt.issuer.[issuer shortname]
- all properties relevant for a particular issuer must be listed under a short name for that issuer (not the actual issuer value from the token, but a chosen name to represent config for the actual issuer) you trust, e.g.citizen
oremployee
no.nav.security.jwt.issuer.[issuer shortname].discoveryurl
- The identity provider configuration/discovery endpoint (metadata)no.nav.security.jwt.issuer.[issuer shortname].accepted_audience
- The value of the audience (aud) claim in the JWT token. For OIDC it is the client ID of the client responsible for acquiring the token, in OAuth 2.0 it should be the identifier for you api.no.nav.security.jwt.issuer.[issuer shortname].validation.optional-claims
- A comma separated list of optional claims that will be excluded from default claims.no.nav.security.jwt.issuer.[issuer shortname].jwks-cache.lifespan
- Cache the retrieved JWK keys to speed up subsequent look-ups. A non-negative lifespan expressed in minutes. (Default 15 min)no.nav.security.jwt.issuer.[issuer shortname].jwks-cache.refreshtime
- A non-negative refresh time expressed in minutes. (Default 5 min)
Each issuer can be configured to use or not use a proxy by specifying the following properties:
no.nav.security.jwt.issuer.[issuer shortname].proxyurl
- The full url of the proxy, e.g. http://proxyhost:8088
Provides core OAuth 2.0 client support, supporting the following OAuth 2.0 grants:
jwt_bearer
client_credentials
token_exchange
This module can be used standalone (e.g. if you do not use Spring or Ktor). You will however need to code the part to "hook in" the client and trigger the token retrieval. If you use Spring you should use the specific wrapper module.
When requesting a token from an OAuth 2.0 Authorization Server the client must authenticate itself with one of the following client authentication methods:
private_key_jwt
client_secret_post
client_secret_basic
The module will choose the client authentication method based on provided configuration in the class ClientProperties
.
Use the interface JwtBearerTokenResolver
to supply a JWT that should be exchanged for another, i.e. in jwt_bearer
or token_exchange
grants.
The OAuth2HttpClient
interface lets you provide a HTTP client of your choosing to perform the actual POST request to the OAuth 2.0 server.
Spring Boot wrapper for the module token-client-core, providing auto configuration for Spring.
Simply annotate your SpringApplication class or any Spring Configuration class with EnableOAuth2Client
.
Enable caching for the OAuth 2.0 access_token
response with cacheEnabled = true
and configurable cacheEvictSkew
and cacheMaximumSize
properties.
@EnableOAuth2Client(cacheEnabled = true)
@Configuration
public class Configuration {
// ...
}
Detailed samples are available in the token-client-spring-demo module.
Not implemented as of now. See demo application in token-client-ktor-demo
.
Add the module that you need as dependencies.
- token-client-spring:
<dependency>
<groupId>no.nav.security</groupId>
<artifactId>token-client-spring</artifactId>
<version>${token-support.version}</version>
</dependency>
- token-client-core (included as dependency of the above):
<dependency>
<groupId>no.nav.security</groupId>
<artifactId>token-client-core</artifactId>
<version>${token-support.version}</version>
</dependency>
no.nav.security.jwt.client.registration[client shortname]
- All properties relevant for a particular client must be listed under ashort name
for that clientno.nav.security.jwt.client.registration[client shortname].token-endpoint-url
- The identity provider /token endpoint, to retrieve a tokenno.nav.security.jwt.client.registration[client shortname].grant-type
- The OAuth 2.0 grant_type to use. Accepted grant types:urn:ietf:params:oauth:grant-type:jwt-bearer
client_credentials
urn:ietf:params:oauth:grant-type:token-exchange
no.nav.security.jwt.client.registration[client shortname].scope
- OAuth 2.0 scopes provide a way to limit the amount of access for access tokens
no.nav.security.jwt.client.registration[client shortname].authentication
- All properties relevant for client authentication must be listed underauthentication
.no.nav.security.jwt.client.registration[client shortname].authentication.client-id
- The client ID for your application (usually preregistered at your OAuth 2.0 authorization server.no.nav.security.jwt.client.registration[client shortname].authentication.client-auth-method
- Standard methods for client authentication. Supported methods are:client_secret_basic
client_secret_post
private_key_jwt
no.nav.security.jwt.client.registration[client shortname].authentication.client-secret
- The client secret must be kept confidential.no.nav.security.jwt.client.registration[client shortname].authentication.client-jwk
-client-jwk
- Used to sign the client JWT, instead ofclient-secret
for authentication against a provider.
no.nav.security.jwt.client.registration[client shortname].token-exchange
- All properties relevant for the grant token-exchange must be listed undertoken-exchange
.no.nav.security.jwt.client.registration[client shortname].token-exchange.audience
- The logical name of the target service where the client intends to use the requested security token.
There is a separate module in token-validation-spring-test for Spring apps, see separate README for more information.
For apps not using Spring we recommend you use the mock-oauth2-server and setup your tests/properties accordingly.
In order to release a new version go to https://github.com/navikt/token-support/releases and click edit on the draft release. Edit or approve the changelog and click publish. Github Action will trigger a new release.
First, it will appear in Sonatype OSS releases, before eventually (a couple of minutes later) it is synced to Maven Central.
If you have any questions, please open an issue on the Github issue tracker.
For NAV employees, you can ask questions at the Slack channel #token-support