tomcat-jwt-security
This project aims to bring JWT token authentication capabilities into Tomcat 8, implementing an authentication filter as a Tomcat Valve. JWT manipulation is based on java-jwt project.
For Tomcat 7, please use version 1.1.0 or clone tomcat-7 branch.
Valve-based authentication is supposed to work along with Java standard security constraints placed in your web.xml file and will leave your server stateless: with a JWT token you can keep your Tomcat free of http session.
From version 3.0.0, several improvements have been made (with many breaking changes - please refers to release notes). Now you can take advantages of signing and verifying your JWT tokens with:
- HMAC algorithms, providing a secret text (the legacy approach, available since versions 2.x.x)
- RSA algorithms, providing a keystore with a public key
- OpenID Connect (OIDC) JWT ID Tokens are now validated against public keys downloaded from a valid JWKS uri, provided by your OIDC Identity Provider
Getting started
You can download artifacts (1.a) or build the project on your own (1.b), then configure Tomcat and your security constraints in your project to enable authentication system.
Finally, read how to create tokens in your app, if you sign your tokens with HMAC or RSA.
1.a Download artifacts
Download artifacts (project and dependencies), from Maven Central Repo, when using HMAC (HmacJwtTokenValve
) or RSA (RsaJwtTokenValve
) valves:
- tomcat-jwt-security-3.0.0.jar
- java-jwt-3.9.0.jar
- jackson-databind-2.10.1.jar (this may cause problems with some Tomcat version)
- commons-codec-1.12.jar
- java-jwt-3.9.0.jar
If you need to use OpenID Connect valve (OidcJwtTokenValve
), you need further dependencies:
Place dependencies into TOMCAT_HOME/lib directory
1.b Build project
You can build with a simple
mvn install
and grab artifacts from target/to-deploy folder. Copy generated artifacts into TOMCAT_HOME/lib directory
2. Register Valve
Now it's time to register the valve. According to your signing method or token provider, from version 3.0.0 you can choose proper valve, according to your scenario:
-
HmacJwtTokenValve
: to be used when tokens are signed with HMAC, based on a pre-shared secret text. Configurable parameters are:Parameter Type Mandatory Default Description secret
String Y Passphrase used to verify the token sign. Since HMAC is a sync algorithm, it's also used to recreate and sign the token when updateExpire
istrue
updateExpire
Boolean N false
Each request produces a new token in X-Auth
response header with a delayed expire time. This simulates default Servlet HTTP Session behaviourcookieName
String N Name of the cookie containing JWT token instead of HTTP headers customUserIdClaim
String N userId
Claim that identify the user id customRolesClaim
String N roles
Claim that identify user capabilities Example
<Valve className="it.cosenonjaviste.security.jwt.valves.HmacJwtTokenValve" secret="my super secret password" updateExpire="true" cookieName="auth" />
-
RsaJwtTokenValve
: to be used when tokens are signed with RSA, based on certificates pairs. Configurable parameters are:Parameter Type Mandatory Default Description keystorePath
String Y* Keystore file system path keystorePassword
String Y* Keystore password keyPairsAlias
String N the first one in keystore Keys pairs alias in keystore. If not provided, the first public key in keystore will be used keyStore
Keystore Y** Keystore instance (useful when keystore is in classpath and is java-based configured) cookieName
String N Name of the cookie containing JWT token instead of HTTP headers customUserIdClaim
String N userId
Claim that identify the user id customRolesClaim
String N roles
Claim that identify user capabilities Mandatory groups (*) and (**) are mutually exclusive:
keyStore
param takes precedence.Example
<Valve className="it.cosenonjaviste.security.jwt.valves.RsaJwtTokenValve" keystorePath="/etc/keystores/keystore.jks" keystorePassword="ks_password" customUserIdClaim="sub" customRolesClaim="authorities" />
-
OidcJwtTokenValve
: to be used when tokens are provided by an OpenID Connect Identity Provider (OIDC IDP). Configurable parameters are:Parameter Type Mandatory Default Description issuerUrl
URL Y URL where to retrieve IDP keys: it's the value of jwks_uri
key of.well-known/openid-configuration
endpoint provided by your IDPsupportedAudiences
String N Allowed aud
values in token. IfsupportedAudiences
is not set, no validation is performedexpiresIn
Integer N 60 Cache duration of keys before recontact IDP for new keys timeUnit
TimeUnit N MINUTES Cache time unit. Allowed values are: NANOSECONDS
,MICROSECONDS
,MILLISECONDS
,SECONDS
,MINUTES
,HOURS
,DAYS
customUserIdClaim
String N sub
Claim that identify the user id customRolesClaim
String N authorities
Claim that identify user capabilities Example
<Valve className="it.cosenonjaviste.security.jwt.valves.OidcJwtTokenValve" issuerUrl="http://idp.example.com/openid-connect/certs" expiresIn="30" timeUnit="MINUTES" />
Valves can be configured in Tomcat in:
server.xml
for registering valve at Engine or Host levelcontext.xml
for registering valve at application Context level
In order for the valve to work, a realm should be provided. An example for a JDBCRealm can be found on a post on TheJavaGeek
3. Enable security-contraint
All these valves check if requested url is under security constraints. So, valve behaviour will be activated only if your application web.xml file contains something like this:
<security-constraint>
<web-resource-collection>
<web-resource-name>api</web-resource-name>
<url-pattern>/api/*</url-pattern>
</web-resource-collection>
<auth-constraint>
<role-name>*</role-name>
</auth-constraint>
</security-constraint>
<security-role>
<role-name>admin</role-name>
</security-role>
<security-role>
<role-name>devop</role-name>
</security-role>
<login-config>
<auth-method>BASIC</auth-method>
<realm-name>MyAppRealm</realm-name>
</login-config>
Please note <auth-method>
tag: is set to BASIC in order to avoid HTTP Session creation on server side. If you omit <auth-method>
or use another authentication method, Tomcat will create an HTTP session for you, but we want our server stateless!
Now your server is ready. How to generate a token from your app?
How to integrate in your project
HMAC and RSA
HmacJwtTokenValve
and RsaJwtTokenValve
inherits from an abstract JwtTokenValve
that is supposed to search for authentication token according to these priorities:
- in
X-Auth
header param - in
Authorization
header param with token preceded byBearer
type - in
access_token
query parameter (useful for downloading a file for example) - in a
cookie
: cookie's name is set by valve parameter cookieName
Your login controller must create a token in order to be validated: each following request to protected application must contain one of the authentication methods above.
You can use classes provided by java-jwt project (recommended), for example:
String token = JWT.create()
.withSubject(securityContext.getUserPrincipal().getName())
.withArrayClaim("authorities", new String[]{"admin", "devop"})
.withIssuedAt(new Date())
.sign(Algorithm.HMAC256("my super secret password"));
...
response.addHeader(JwtConstants.AUTH_HEADER, token);
or our utility classes such as JwtTokenBuilder
or JwtConstants
(legacy): tomcat-jwt-security is also available on Maven Central. You can include it in your project as provided dependency (because is in your TOMCAT_HOME/lib folder already!):
<dependency>
<groupId>it.cosenonjaviste</groupId>
<artifactId>tomcat-jwt-security</artifactId>
<version>3.0.0</version>
<scope>provided</scope>
</dependency>
And use it like this in your login controller:
String token = JwtTokenBuilder.create(Algorithm.HMAC256("my super secret password"))
.userId(securityContext.getUserPrincipal().getName())
.roles(Arrays.asList("admin", "devop"))
.expirySecs(1800)
.build();
...
response.addHeader(JwtConstants.AUTH_HEADER, token);
OpenID Connect
In case of OidcJwtTokenValve
, you don't need a login controller, since you are just protecting your APIs as OAuth2 Resource Server. Authentication is handled elsewhere to gather the JWT: just configure the valve and send obtained JWT token in every HTTP header Authorization
, preceded by Bearer
.
Enjoy now your stateless security system on Tomcat!!
JWT Valve and Spring Boot
If you want to use this Valve with Embedded Tomcat provided by Spring Boot, forget about web.xml
or context.xml
! Just include as Maven dependency in compile
scope and implement a EmbeddedServletContainerCustomizer
like this:
@Configuration
public class TomcatJwtSecurityConfig implements EmbeddedServletContainerCustomizer {
@Override
public void customize(ConfigurableEmbeddedServletContainer container) {
if (container instanceof TomcatEmbeddedServletContainerFactory) {
TomcatEmbeddedServletContainerFactory factory = (TomcatEmbeddedServletContainerFactory) container;
factory.addContextValves(newJwtTokenValve());
factory.addContextValves(new BasicAuthenticator());
factory.addContextCustomizers(context -> {
context.setRealm(newJdbcRealm());
// replace web.xml entries
context.addConstraint(unsecured());
context.addConstraint(secured());
context.addSecurityRole("admin");
context.addSecurityRole("devop");
});
}
}
private SecurityConstraint unsecured() {
SecurityCollection collection = new SecurityCollection("login", "login");
collection.addPattern("/api/login");
SecurityConstraint securityConstraint = new SecurityConstraint();
securityConstraint.addCollection(collection);
return securityConstraint;
}
private SecurityConstraint secured() {
SecurityCollection collection = new SecurityCollection("api", "api");
collection.addPattern("/api/*");
SecurityConstraint securityConstraint = new SecurityConstraint();
securityConstraint.addAuthRole("*");
securityConstraint.setAuthConstraint(true);
securityConstraint.addCollection(collection);
return securityConstraint;
}
private HmacJwtProvider newJwtTokenValve() {
HmacJwtProvider valve = new HmacJwtProvider();
valve.setSecret("my-secret");
valve.setUpdateExpire(true);
return valve;
}
private Realm newJdbcRealm() {
// your favourite realm
}
}
Some notes:
- this is a programmatic version of
web.xml
configuration (andcontext.xml
) BasicAuthenticator
is required becauseJwtTokenValve
is not an authenticator:BasicAuthenticator
mainly delegates login phase to registered Realm in Tomcat Context.