What you will learn

In this tutorial we’ll have at look at Sessions. We’ll learn about what they’re used for, as well as different ways of persisting, caching and clustering sessions using Jetty.

What is a session?

When a user visits a website (or opens a webapp), the server usually creates a Session object for the user. Users are usually linked to their Session by cookie with a sessionId value. This Session object can be used to store information about the current, well, session, like if the user is logged in.

The architecture of session management changed significantly in Jetty 9.4, and this tutorial is intended to get you up to speed. You can view the documentation on Jetty’s website if you need all the details.

Persisting sessions

By default Jetty will store all its session information in a HashMap, which is stored in memory (RAM). When the Jetty server restarts all of the sessions are cleared. Restarts can happen for example if you’re making changes on localhost, or if you’re deploying a new version of your app to your cloud provider.

Please note: In order for Jetty to successfully persist your sessions, all objects in your session attributes must implement the Serializable interface. This is usually as simple as adding implements Serializable to your classes.

Persisting to the file system

The simplest way to persist a Session is to store the Sessions as files on the file system. This can be done using a FileSessionDataStore.

This approach is well suited for a dev environment, since it’s easy to set up and has no dependencies. You need to create a SessionHandler with a SessionCache, and attach a FileSessionDataStore:

  • Java
  • Kotlin
public static SessionHandler fileSessionHandler() {
    SessionHandler sessionHandler = new SessionHandler();
    SessionCache sessionCache = new DefaultSessionCache(sessionHandler);
    sessionCache.setSessionDataStore(fileSessionDataStore());
    sessionHandler.setSessionCache(sessionCache);
    sessionHandler.setHttpOnly(true);
    // make additional changes to your SessionHandler here
    return sessionHandler;
}

private static FileSessionDataStore fileSessionDataStore() {
    FileSessionDataStore fileSessionDataStore = new FileSessionDataStore();
    File baseDir = new File(System.getProperty("java.io.tmpdir"));
    File storeDir = new File(baseDir, "javalin-session-store");
    storeDir.mkdir();
    fileSessionDataStore.setStoreDir(storeDir);
    return fileSessionDataStore;
}
fun fileSessionHandler() = SessionHandler().apply {
    sessionCache = DefaultSessionCache(this).apply {
        sessionDataStore = FileSessionDataStore().apply {
            val baseDir = File(System.getProperty("java.io.tmpdir"))
            this.storeDir = File(baseDir, "javalin-session-store").apply { mkdir() }
        }
    }
    httpOnly = true
    // make additional changes to your SessionHandler here
}

This approach can also work on a remote server, but some cloud providers wipe all files when you redeploy your service, so be careful. File IO can also be slow, depending on your hardware. If you want your sessions to be a bit more persistent, and faster, you can use a database.

Persisting to a database

Programmatically, persisting to a database is not very different from persisting to the file system. You need to create a SessionHandler with a SessionCache, but instead of using a FileSessionDataStore you need to use a datastore specific for your database. Here is an example using JDBC:

  • Java
  • Kotlin
public static SessionHandler sqlSessionHandler(String driver, String url) {
    SessionHandler sessionHandler = new SessionHandler();
    SessionCache sessionCache = new DefaultSessionCache(sessionHandler);
    sessionCache.setSessionDataStore(
        jdbcDataStoreFactory(driver, url).getSessionDataStore(sessionHandler)
    );
    sessionHandler.setSessionCache(sessionCache);
    sessionHandler.setHttpOnly(true);
    // make additional changes to your SessionHandler here
    return sessionHandler;
}

private static JDBCSessionDataStoreFactory jdbcDataStoreFactory(String driver, String url) {
    DatabaseAdaptor databaseAdaptor = new DatabaseAdaptor();
    databaseAdaptor.setDriverInfo(driver, url);
    // databaseAdaptor.setDatasource(myDataSource); // you can set data source here (for connection pooling, etc)
    JDBCSessionDataStoreFactory jdbcSessionDataStoreFactory = new JDBCSessionDataStoreFactory();
    jdbcSessionDataStoreFactory.setDatabaseAdaptor(databaseAdaptor);
    return jdbcSessionDataStoreFactory;
}
fun sqlSessionHandler(driver: String, url: String) = SessionHandler().apply {
    sessionCache = DefaultSessionCache(this).apply {
        sessionDataStore = JDBCSessionDataStoreFactory().apply {
            setDatabaseAdaptor(DatabaseAdaptor().apply {
                setDriverInfo(driver, url)
                // setDatasource(myDataSource) // you can set data source here (for connection pooling, etc)
            })
        }.getSessionDataStore(sessionHandler)
    }
    httpOnly = true
    // make additional changes to your SessionHandler here
}

If you want to use MongoDB you can just switch the data store:

  • Java
  • Kotlin
private static MongoSessionDataStoreFactory mongoDataStoreFactory(String url, String dbName, String collectionName) {
    MongoSessionDataStoreFactory mongoSessionDataStoreFactory = new MongoSessionDataStoreFactory();
    mongoSessionDataStoreFactory.setConnectionString(url);
    mongoSessionDataStoreFactory.setDbName(dbName);
    mongoSessionDataStoreFactory.setCollectionName(collectionName);
    return mongoSessionDataStoreFactory;
}
sessionDataStore = MongoSessionDataStoreFactory().apply {
    connectionString = "..."
    dbName = "..."
    collectionName = "..."
}.getSessionDataStore(sessionHandler)

Jetty supports JDBC, MongoDB, Inifinspan, Hazelcast, and Google Cloud DataStore. JDBC is included with in the core jetty-server dependency, while MongoDB and others require additional dependencies.

If you’re using a SQL database, Jetty will create a jettysessions table. If you’re using MongoDB it will create a document. They both contain the same data:

{
    "_id": {
        "$oid": "5b858d527d3c0f8722173292"
    },
    "id": "node0j4ii2zxu01i91g5f8odup78c30",
    "accessed": 1535479586876,
    "context": {
        "0_0_0_0:": {
            "__metadata__": {
                "lastNode": "node0",
                "lastSaved": 1535479586879,
                "version": 78
            },
            "signed-in-user": "tipsy" // custom data
        }
    },
    "created": 1535479122617,
    "expiry": 0,
    "lastAccessed": 1535479585053,
    "maxIdle": -1,
    "valid": true
}

For performance and security reasons, it’s advised to create a separate database (on the database instance) with credentials unique to Jetty.

Session cache and clustering

Since database and file systems operations are relatively slow, caching data in memory can increase the performance of your application. In both the previous examples we attached a SessionCache to our SessionHandler. There are two implementations of the SessionCache included in Jetty, DefaultSessionCache and NullSessionCache.

DefaultSessionCache

We used the DefaultSessionCache in our previous examples, and it caches sessions in memory. This is great if you have one instance of your app running, but it can lead to trouble if you have two instances behind a load-balancer. Jetty recommends you always use sticky-sessions with the DefaultSessionCache, but even with sticky sessions you can run into inconsistencies. If an instance goes down or gets overloaded, traffic will be routed to the other instance which won’t have the same session in its cache. This is where the NullSessionCache is useful.

NullSessionCache

The NullSessionCache doesn’t actually do any caching at all. Every time a Session is needed it’s fetched from the SessionDataStore. This means that all instances share the same source of truth, and there won’t be any inconsistencies. Jetty recommends this approach for clustering without sticky-sessions, but it’s the safer choice even if sticky-sessions are enabled.

There is a performance penalty to not caching, but if you’re running a dedicated database on the same network with small sessions, it should just be ~10ms per request. Using an external hosted MongoDB such as mlab it seems to be around 40ms.

By default, Jetty uses lenient cookie security settings. In order to harden and to mitigate cross-site request forgery (CSRF) attacks, it is useful to set the SameSite=strict cookie flag. This is particularly recommended if the JSESSIONID cookie is also used directly or indirectly for authentication purposes.

  • Java
  • Kotlin
static SessionHandler customSessionHandler() {
    final SessionHandler sessionHandler = new SessionHandler();
    sessionHandler.setHttpOnly(true);
    sessionHandler.setSecureRequestOnly(true);
    sessionHandler.setSameSite(HttpCookie.SameSite.STRICT);
    return sessionHandler;
}
fun customSessionHandler() = SessionHandler().apply {
    httpOnly = true
    isSecureRequestOnly = true
    sameSite = HttpCookie.SameSite.STRICT
}

Summary

  • Session handling in Jetty requires a SessionHandler, SessionCache, and a SessionDataStore
  • A FileSessionDataStore is well suited for development environments
  • The DefaultSessionCache works well if you only run one instance of your app
  • The NullSessionCache is suitable for multiple instances running behind a loadbalancer

Usage in Javalin

Since you are currently on javalin.io, it should be mentioned how to use this knowledge in your Javalin app. Since Javalin relies on Jetty for session handling can, you simply pass your SessionHandler:

  • Java
  • Kotlin
var app = Javalin.create(config -> {
    config.jetty.modifyServletContextHandler(handler -> handler.setSessionHandler(fileSessionHandler()));
}).start(7070);
val app = Javalin.create { config ->
    config.jetty.modifyServletContextHandler { it.sessionHandler = fileSessionHandler() }
}.start(7070)

As we saw earlier, the SessionHandler has a SessionCache which again has a SessionDataStore, so no further configuration is required. All session configuration happens through Jetty classes.

N.B. some browsers (see e.g. here) will eventually further restrict cookies to first-party access by default in case the SameSite and secure cookie settings are not being set.

Working with sessions

Sessions are a great way to keep a trusted state for your connected clients. If you use a session database, values stored in the session store can be retrieved by each of your running instances.

Writing values

  • Java
  • Kotlin
app.get("/write", ctx -> {
    // values written to the session will be available on all your instances if you use a session db
    ctx.sessionAttribute("my-key", "My value");
});
app.get("/write") { ctx ->
    // values written to the session will be available on all your instances if you use a session db
    ctx.sessionAttribute("my-key", "My value")
}

Reading values

  • Java
  • Kotlin
app.get("/read", ctx -> {
    // values on the session will be available on all your instances if you use a session db
    String myValue = ctx.sessionAttribute("my-key");
});
app.get("/read") { ctx ->
    // values on the session will be available on all your instances if you use a session db
    val myValue = ctx.sessionAttribute<String>("my-key")
}

Invalidating sessions

  • Java
  • Kotlin
app.get("/invalidate", ctx -> {
    // if you want to invalidate a session, jetty will clean everything up for you
    ctx.req().getSession().invalidate();
});
app.get("/invalidate") { ctx ->
    // if you want to invalidate a session, jetty will clean everything up for you
    ctx.req().session.invalidate()
}

Changing session ids

  • Java
  • Kotlin
app.get("/change-id", ctx -> {
    // it could be wise to change the session id on login, to protect against session fixation attacks
    ctx.req().changeSessionId();
});
app.get("/change-id") { ctx ->
    // it could be wise to change the session id on login, to protect against session fixation attacks
    ctx.req().changeSessionId()
}