You might not be able to avoid them (e.g. integrat...
# ktor
m
You might not be able to avoid them (e.g. integration with a third party product that requires them) but keep in mind that JWT & friends have an unreasonably high risk of shooting yourself in the foot
s
how many projects can you name that actually had security problems because of jwt
d
So what's better than jwt (without the added complexity of oauth)?
m
@Saša Šijak Any project using any of the various JWT libraries that have had serious vulnerabilities (arguably because the spec is needlessly complex and difficult to implement correctly)
d
Interesting. What do you do to authenticate in an api? It would be nice if Ktor would provide some more battle tested types of authentication...
m
I’d just use sessions. Put your session storage in Redis and that’ll serve you well for quite a while.
d
With an existing Ktor Feature? Or some specific lib that needs to be adapted?
m
Ktor’s session support has been working just fine for me. I wrote a trivial redis adapter using lettuce based on the session storage docs. LMK if you want that code
Just the regular feature.
d
The session support handles authentication?
Or you use basic authentication and then save a session for next requests? But then, you need extra logic for expiring those sessions... and if, say it's an Android device, then it would need logic to save that session, and get a new one. Alot of REST APIs are stateless nowadays...
m
It’s part of it. Say you take a username and password on your site. Have an endpoint that receives u&p which validated the pair using normal password best practices. If it all checks out, use a basic data class that stores something like the user id etc, and write that instance into the session. Then, use the authentication feature to check the session and provide a principal.
Redis TTL will handle expiration nicely.
d
True
m
How you communicate the session is up to you. Could use cookies or headers or whatever
I use cookies on my service because the client is a web UI and cookies are the most secure option for that.
d
+ basic authentication?
I guess with ssl it's more secure than JWT according to that article 🙂
m
No, http basic would throw up the ugly browser username and password dialog. There’s a page with a html form that posts JSON which an endpoint handles
And yes all is TLS always
d
I guess for an API, it would be more appropriate... I'm not working with browsers too much... thanks for the links and all!
m
Sure. 1 min and I’ll paste some snippets that hopefully will make it more clear
d
If you could pass me that adapter, it would be great! Any reason you're using lettuce and not jedis? Isn't it a bit overkill?
m
I don't recall exactly why I chose lettuce
d
It has tons of features and an async api, but for such a small need, might not be worth the overhead...
m
better async api? better maintenance schedule? who knows
Copy code
class RedisSessionStorage(private val redis: RedisAsyncCommands<String, ByteArray>,
                          ttl: Duration,
                          private val keyPrefix: String = "session_") : SimplifiedSessionStorage() {

    private val ttlMillis = ttl.toMillis()

    override suspend fun read(id: String): ByteArray? {
        return redis.get(effectiveId(id))
                .asDeferred()
                .await()?.also {
                    redis.pexpire(effectiveId(id), ttlMillis)
                            .asDeferred()
                            .await()
                }
    }

    override suspend fun write(id: String, data: ByteArray?) {
        if (data == null) {
            redis.del(effectiveId(id))
        } else {
            redis.set(effectiveId(id), data, SetArgs().px(ttlMillis))
        }.asDeferred()
                .await()
    }

    private fun effectiveId(id: String): String = keyPrefix + id

}

class StringByteCodec : RedisCodec<String, ByteArray> {
    private val keyCodec = StringCodec()
    private val valCodec = ByteArrayCodec()

    override fun decodeKey(bytes: ByteBuffer?): String = keyCodec.decodeKey(bytes)

    override fun encodeValue(value: ByteArray?): ByteBuffer = valCodec.encodeValue(value)

    override fun encodeKey(key: String?): ByteBuffer = keyCodec.encodeKey(key)

    override fun decodeValue(bytes: ByteBuffer?): ByteArray = valCodec.decodeValue(bytes)
}

/**
 * Since we don't have an obvious place to hang on to re-usable buffers, use this helper from
 * <https://ktor.io/features/sessions.html>. It does allocate, but oh well...
 */
abstract class SimplifiedSessionStorage : SessionStorage {
    abstract suspend fun read(id: String): ByteArray?
    abstract suspend fun write(id: String, data: ByteArray?)

    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(CoroutineScope(<http://Dispatchers.IO|Dispatchers.IO>).reader(coroutineContext, autoFlush = true) {
            val data = ByteArrayOutputStream()
            val temp = ByteArray(1024)
            while (!channel.isClosedForRead) {
                val read = channel.readAvailable(temp)
                if (read <= 0) break
                data.write(temp, 0, read)
            }
            write(id, data.toByteArray())
        }.channel)
    }
}
Plug that in as your session storage and you're done.
👍🏼 1
something like
data class SessionData(val userId: UUID)
would be suitable for what you store in the session.
In your log in endpoint, once you've done whatever validation you want to do on username/password,
call.sessions.set(SessionData(theUserId))
Then, in your
install(Authentication)
block, you'd do
session<SessionData>("USER_AUTH")  { lookUpUserFromSession(it)?.let { user -> SomeUserPrincipalClass(user.foo, user.bar, user.baz) }
whatever principal type you wish to use is what you'll get back out of
call.authentication.principal<SomeUserPrincipalClass>()
. If you have roles or something like that associated with users, that type would be a good place to hold them since you will have just loaded your user record.
d
Nice 😉