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.
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()
}
}