Git Product home page Git Product logo

tomcat-jwt-security's Introduction

Build Status Released Version

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:

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 is true
    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 behaviour
    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

    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 IDP
    supportedAudiences String N Allowed aud values in token. If supportedAudiences is not set, no validation is performed
    expiresIn 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 level
  • context.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 by Bearer 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 (and context.xml)
  • BasicAuthenticator is required because JwtTokenValve is not an authenticator: BasicAuthenticator mainly delegates login phase to registered Realm in Tomcat Context.

tomcat-jwt-security's People

Contributors

andreacomo avatar dependabot[bot] avatar ssblanco avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar

tomcat-jwt-security's Issues

Question: API against OpenId / OAuth2

Hey Andrea,

Thanks for the valve. It's a very nice tool. I have a question.

I want to use your valve to autenticate an stateless api against an oauth2 server.
In my case, the api is under my responsibility but the oauth2 server is not under my responsibility so i can't share the secret key.

I see in the README.md some openid functionalities.
So the question is: Can I use the valve to autenticate my API against a public OAuth2 server (google or github for example)? And what are the best approach to configure the valve?

Thanks.

'nbf' not handled correctly

I think JwtTokenBuilder.java:145 should be computing the difference between the 'iat' (issuedAt) time and the 'nbf' (notBeforeTime), but it is computing the sum instead.

e.g. (using small values for the sake of clarity) if 'iat' is 1000 and 'nbf' is 900, the leeway should be 100 (issuedAt - notBefore), not 1900 (notBefore + issuedAt).

RFC: Update java-jwt to latest version 3.8.1

Summary

Update java-jwt to 3.8.1

Motivation

  • We would like to use this library for JWT authentication on our Tomcat server and we would like to use java-jwt to read custom claims, but the latest version is incompatible with 2.3.0
  • 3.8.1 uses a cleaner API and could possibly eliminate the need for the JwtTokenBuilder and JwtTokenVerifier classes
  • 3.8.1 fixes security issues and has performance enhancements
  • java-jwt v2.x.x was last updated over two years ago

Detailed design

Break Backwards Compatibility

The latest version of java-jwt includes an easy to use builder to do token creation and verification. You can verify a token with:

JWTVerifier verifier = JWT.require(Algorithm.HMAC256(secret)).build();
verifier.verify(token);

example

And you can similarly create a token with JWT.create(). This means JWTTokenBuilder and JWTTokenVerifier could be removed.

Keeping Backwards Compatibility

If you wish to keep the latest version backwards compatible with the existing APIs, then a bit more work would need to be done.

JwtTokenVerifier
Internally it would still hold an instance of com.auth0.jwt.JWTVerifier, but the API has changed. It would be initialized with JWT.require(Algorithm.HMAC256(secret)).build();. The method JWTVerifier.verify(token) now returns a DecodedJWT object instead of Map<String, Object>. The DecodedJWT object gives access to claims.

JwtTokenBuilder
JWTSigner and Options classes no longer exist. This means JwtTokenBuilder would need to store the options itself, and then apply them when the build functions is called. Using the new JWT.create() tool, it could look something like this:

public String build() {
        JWTCreator.Builder jwtBuilder = JWT.create();
	Calendar calendar = Calendar.getInstance();
	Date currentDate = new Date();
	if (issuedAt) {
		jwtBuilder.withIssuedAt(currentDate);
	}
	if (expirySeconds != null) {
		calendar.setTime(currentDate);
		calendar.add(Calendar.SECOND, expirySeconds);
		jwtBuilder.withExpiresAt(calendar.getTime());
	}
	if (jwtId) {
		jwtBuilder.withJWTId(UUID.randomUUID().toString());
	}
	if (notValidBeforeLeeway != null) {
		calendar.setTime(currentDate);
		calendar.add(Calendar.SECOND, -1 * notValidBeforeLeeway);
		jwtBuilder.withNotBefore(calendar.getTime());
	}
	return jwtBuilder.sign(algorithm);
}

Drawbacks

Break Backwards Compatibility

Self explanatory, current users of the library may need to update a significant portion of their code.

Keeping Backwards Compatibility

Could be a pain to fit the current design of tomcat-jwt-security to fit around 3.8.1

Adoption strategy

If backwards compatibility is broken, create a new major version so people know.

Implementation

I would like to contribute to this project if an agreement can be made on updating to the latest version of java-jwt 😃

crash on tomcat 8

Hi. I've followed all the steps in readme.md, but am still unable to run my app. This is the stacktrace

screencapture-localhost-8080-public-index-html-1447273561374

custom login form

Hi.

First of all, this projects looks great!.

I am thinking in use it in my app, but i have a cuestion. I understand that i can have my custom login form that will be not protected and it must generate the x-auth header.

But if a user try to access to a protected resource without the header....
my app will show the awfull basic login popup? is it possible instead redirect to my customized login dialog ?

Token Generation

First of all thanks for creating this project. It really helped me to implement Access token in tomcat.

I have some area of improvements for this. At present to implement this feature in the project, I need register valve in tomcat and then need to make update on web application to create the auth header post authentication. One problem I faced is I when used “UserDatabaseRealm” (using tomcat-user.xml) with BASIC authentication. Even thought in the in web.xml, I said basic authentication, it never showed login popup. Instead it gave me error saying please login first.

Ideally anything do we with security should be handled by servlet container Realm. We should not be writing any code in web application related to security. In our case, once user authenticate with tomcat realm, JWTValue should generate the token if it does not exists.

I have extended the JwtTokenValve class and added below code to auto generate the token if it does not exists. Let me know this enhancement is helpful. If you agree, we can update core classes.

@OverRide
public void invoke(Request request, Response response) throws IOException, ServletException {
if(getToken(request) == null) {
//if user is not logged in, propagate the request and let container authenticate
if(request.getUserPrincipal() == null) {
this.getNext().invoke(request, response);
}
//for basic authentication, there will be some back and forth. Hence check once more of user is validated by container.
if(request.getUserPrincipal() != null){
JwtTokenBuilder tokenBuilder = JwtTokenBuilder.create(secret);
String token = tokenBuilder.userId(request.getUserPrincipal().getName())
.roles(Arrays.asList("admin"))
.expirySecs(1800)
.build();
response.addHeader(JwtConstants.AUTH_HEADER, token);
}
}else {
//if token is found, validate the token
super.invoke(request, response);
}
}

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.