Cookie/Header Sessions

Estimated reading time: 4 minutes

Transfer using Cookies or Custom Headers

You can either use cookies or custom HTTP headers for sessions. The code is roughly the same but you have to call either the cookie or header method, depending on where you want to send the session information.

Cookies vs Headers sessions

Depending on the consumer, you might want to transfer the sessionId or the payload using a cookie, or a header. For example, for a website, you will normally use cookies, while for an API you might want to use headers.

The Sessions.Configuration provide two methods cookie and header to select how to transfer the sessions:

Cookies

application.install(Sessions) {
    cookie<MySession>("SESSION")
} 

You can configure the cookie by providing an additional block. There is a cookie property allowing to configure it, for example by adding a SameSite extension:

application.install(Sessions) {
    cookie<MySession>("SESSION") {
        cookie.extensions["SameSite"] = "lax"
    }
} 

The Cookie method is intended for browser sessions. It will use a standard Set-Cookie header. Inside the cookie block, you have access to a cookie property which allows you to configure the Set-Cookie header, for example, by setting a cookie’s path or expiration, domain or https related things.

install(Sessions) {
    cookie<SampleSession>("COOKIE_NAME") {
        cookie.path = "/"
        /* ... */
    }
}

Headers

The Header method is intended for APIs, both for using in JavaScript XHR requests and for requesting them from the server side. It is usually easier for API clients to read and generate custom headers than to handle cookies.

install(Sessions) {
    header<SampleSession>("HTTP_HEADER_NAME") { /* ... */ }
}
application.install(Sessions) {
    header<MySession>("SESSION")
} 

Custom storages

The Sessions API provides a SessionStorage interface, that looks like this:

interface SessionStorage {
    suspend fun write(id: String, provider: suspend (ByteWriteChannel) -> Unit)
    suspend fun invalidate(id: String)
    suspend fun <R> read(id: String, consumer: suspend (ByteReadChannel) -> R): R
}

All three functions are marked as suspend and are designed to be fully asynchronous and use ByteWriteChannel and ByteReadChannel from kotlinx.coroutines.io that provide APIs for reading and writing from an asynchronous Channel.

In your implementations, you have to call the callbacks providing a ByteWriteChannel and a ByteReadChannel that you have to provide: it is your responsibility to open and close them. You can read more about ByteWriteChannel and ByteReadChannel in their libraries documentation. If you just need to load or store a ByteArray, you can use this snippet which provides a simplified session storage:

abstract class SimplifiedSessionStorage : SessionStorage {
    abstract suspend fun read(id: String): ByteArray?
    abstract suspend fun write(id: String, data: ByteArray?): Unit

    override suspend fun invalidate(id: String) {
        write(id, null)
    }

    override suspend fun <R> read(id: String, consumer: suspend (ByteReadChannel) -> R): R {
        val data = read(id) ?: throw NoSuchElementException("Session $id not found")
        return consumer(ByteReadChannel(data))
    }

    override suspend fun write(id: String, provider: suspend (ByteWriteChannel) -> Unit) {
        return provider(reader(coroutineContext, autoFlush = true) {
            write(id, channel.readAvailable())
        }.channel)
    }
}

suspend fun ByteReadChannel.readAvailable(): ByteArray {
    val data = ByteArrayOutputStream()
    val temp = ByteArray(1024)
    while (!isClosedForRead) {
        val read = readAvailable(temp)
        if (read <= 0) break
        data.write(temp, 0, read)
    }
    return data.toByteArray()
}

With this simplified storage you only have to implement two simpler methods:

abstract class SimplifiedSessionStorage : SessionStorage {
    abstract suspend fun read(id: String): ByteArray?
    abstract suspend fun write(id: String, data: ByteArray?): Unit
}

So for example, a redis session storage would look like this:

class RedisSessionStorage(val redis: Redis, val prefix: String = "session_", val ttlSeconds: Int = 3600) :
    SimplifiedSessionStorage() {
    private fun buildKey(id: String) = "$prefix$id"

    override suspend fun read(id: String): ByteArray? {
        val key = buildKey(id)
        return redis.get(key)?.unhex?.apply {
            redis.expire(key, ttlSeconds) // refresh
        }
    }

    override suspend fun write(id: String, data: ByteArray?) {
        val key = buildKey(id)
        if (data == null) {
            redis.del(buildKey(id))
        } else {
            redis.set(key, data.hex)
            redis.expire(key, ttlSeconds)
        }
    }
}