ruby-on-railsbearer-tokenktordevise-jwt

Rails Backend and KotlinJs Ktor Frontend, trying to implement authentication with DeviseJWT


I am reasonably familiar with Rails, as well as KotlinJs, but I am new to doing auth, and I am quite stuck.

I set up Rails Devise, and added JWT as per the tutorials online except that I was only using username and password. I also set up the Ktor frontend as follows:

const val backend = "http://localhost:3000"

val bearerTokenStorage = mutableListOf<BearerTokens>()
suspend fun login(username: String, password: String): Pair<HttpStatusCode, Json> {
    val client = HttpClient(Js) {
        install(Logging)
        install(ContentNegotiation) { json(Json) }
        install(HttpCookies)
//        install(Auth) {
//            bearer {
//                loadTokens {
//                    bearerTokenStorage.last()
//                }
//            }
//        }
    }
    val userData = User(user = UserData(username = username, password = password))

    val response: HttpResponse = client.post("$backend/users/sign_in") {
        headers {
            append(HttpHeaders.AccessControlAllowOrigin, "*")
            append("Credentials", "include")
        }
        contentType(ContentType.Application.Json)
        setBody(userData)
    }
    console.log("Cookies: ", client.cookies("http://0.0.0.0:3000/"), client.cookies("http://0.0.0.0:8080/")) // returns null

    if (response.status == HttpStatusCode.OK) {
        val token = response.headers[HttpHeaders.Authorization]
        console.log("Authorization Header: $token, ${bearerTokenStorage.firstOrNull()}") //returns null
        // Handle successful authentication
    } else {
        console.log("auth failed")
        // Handle authentication failure
    }
//    val jwtToken = client.readToken(response)?.getClaim("yourClaimName")?.asString()
    println(response.bodyAsText())
    client.close()

    return Pair(response.status, JSON.parse(response.body()))
}

suspend fun getDataFromInputsAndSend(inputsContainer: HTMLElement): Json {
    with(inputsContainer) {
        val entries = rows.map { row ->
            SaveEntries(
                startTime = row.startTimeInput.value,
                endTime = row.endTimeInput.value
            )
        }

        val toSend = SaveData(...entries)
        println(toSend)
        return sendData(toSend)
    }
}

suspend fun sendData(toSend: SaveData): Json {
    val client = HttpClient(Js) {
        install(ContentNegotiation) { json(Json) }
        install(HttpCookies)
//        install(Auth) {
//            bearer {
//                loadTokens {
//                    bearerTokenStorage.last()
//                }
//            }
//        }
    }
    val response: HttpResponse = client.post("$backend/maslas") {
        headers {
            append(HttpHeaders.AccessControlAllowOrigin, "*")
            append("Credentials", "include")
        }
        contentType(ContentType.Application.Json)
        setBody(toSend)
    }

    return JSON.parse(response.body())
}

When I log in successfully, the response has: Authorization: Bearer eyJhb... as well as Set-Cookie: _backend_session=%2F1vT...%3D%3D; path=/; HttpOnly; SameSite=Lax. I am told that Set-Cookie is supposed to be saved in the browser in the JS FetchApi via Credentials: Include but I have no idea how to do that in Ktor. Someone suggested sending the token via the body, and even though I am almost certain that is the wrong solution, I would be willing to try it if I knew how to do that.


Solution

  • After a long time of working on this, I realized that my mistake was in the rails backend: I had exposed the header 'Authorization' as per instructions, but I had exposed it in the wrong function, the 'Cors' initializer, rather than the middleware as I was supposed to. Once I exposed the header properly, Ktor was also able to see it, and I was able to store it and retrieve it as desired.

    My final Ktor code is only:

    val BACKEND = Url("http://localhost:3000/")
    const val AUTHORIZATION = "Authorization"
    
    val client by lazy {
        HttpClient(Js) {
            install(ContentNegotiation) { json() }
        }
    }
    
    suspend fun login(username: String, password: String) {
        val userData = User(user = UserData(username = username, password = password))
    
        val response = client.post("$BACKEND/users/sign_in"){
            contentType(ContentType.Application.Json)
            setBody(userData)
        }
    
        val token = response.headers[AUTHORIZATION]
        if (response.status == HttpStatusCode.OK && token != null) {
            localStorage.setItem(AUTHORIZATION, token)
            mainPage()
        }
    }