kotlinx-rpc icon indicating copy to clipboard operation
kotlinx-rpc copied to clipboard

Support for json web token?

Open EricDeng1001 opened this issue 1 year ago • 8 comments
trafficstars

I have a set of Service that must be used after login. I issue jwt to client so they can identify themselves. And I also got a AuthService which issue this token. So how do I setup my rpc server and client for this scenario? For example, how do I carry this jwt in client, and how do I verify this jwt in server? Besides, do I need to setup two paths, one for AuthService, because it does not require jwt, and another for rest of services, which requires jwt?

EricDeng1001 avatar Aug 21 '24 11:08 EricDeng1001

Hi! If you are using kRPC protocol with Ktor integration - they have a JWT plugin for this: https://ktor.io/docs/server-jwt.html#authenticate-route

Otherwise - no builtin solution is available

Mr3zee avatar Aug 21 '24 12:08 Mr3zee

Hello! I am already using this, but encounter a problem: when user not logged in, trying to connect ws returns 401, and service creation is failed. But I must have init these service when my app start. Another problem is, it seems that rpc request is not carrtying bearer token like normal http call. I will post my config code below.

EricDeng1001 avatar Aug 21 '24 13:08 EricDeng1001

client:

object RPC {
    private val client = HttpClient {
        installRPC()
        install(Auth) {
            bearer {
                loadTokens {
                    BearerTokens(UserContext.token, "")
                }
                refreshTokens {
                    BearerTokens(UserContext.token, "")
                }
            }
        }
    }
    private lateinit var rpcClient: RPCClient
    lateinit var authService: AuthService
    lateinit var subscriptionManager: SubscriptionManager
    lateinit var channelManager: ChannelManager
    lateinit var purchaseService: PurchaseService
    lateinit var accountManager: AccountManager
    lateinit var pushManager: PushManager

    suspend fun init() {
        rpcClient = client.rpc {
            url("ws://localhost:3000/api")

            rpcConfig {
                serialization {
                    json()
                }
            }
        }
        authService = client.rpc {
            url("ws://localhost:3000/api/auth")

            rpcConfig {
                serialization {
                    json()
                }
            }
        }.withService()
        subscriptionManager = rpcClient.withService()
        channelManager = rpcClient.withService()
        purchaseService = rpcClient.withService()
        accountManager = rpcClient.withService()
        pushManager = rpcClient.withService()
    }
}

And init is called when App start using LauchedEffect. server:

fun Application.module() {
    install(CORS) {
        allowOrigins { true }
    }
    install(RPC) {

    }
    install(Authentication) {
        jwt {
            verifier(
                AuthContext.verifier
            )
            validate { credential ->
                AuthContext.validate(credential)
            }
            challenge { defaultScheme, realm ->
                call.respond(HttpStatusCode.Unauthorized, "Token is not valid or has expired")
            }
            realm = "Access to maotao"
        }
    }
    install(DoubleReceive)
    install(CallLogging) {
        level = Level.INFO
        format { call ->
            runBlocking {
                val status = call.response.status()
                val httpMethod = call.request.httpMethod.value
                val userAgent = call.request.headers["User-Agent"]
                val body = call.receiveText()
                val uri = call.request.uri
                "$httpMethod $uri $status \n$userAgent \n$body"
            }
        }
    }


    routing {
        pwaDispatch()
        auth()
        authenticate {
            api()
        }
    }
}

And when app is started, server logged:

2024-08-21 21:12:50.065 [eventLoopGroupProxy-4-1] INFO  Application - GET /api 401 Unauthorized 
Mozilla/5.0 (iPhone; CPU iPhone OS 16_6 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.6 Mobile/15E148 Safari/604.1 Edg/127.0.0.0 

EricDeng1001 avatar Aug 21 '24 13:08 EricDeng1001

I was using REST before, and auth is 100% ok. I just switch from normal ktor to rpc.

EricDeng1001 avatar Aug 21 '24 13:08 EricDeng1001

@EricDeng1001 your auth service is under url("ws://localhost:3000/api/auth") protected route:

authenticate {
    api()
}

Hence the error. You can try to move auth service into another route, which is not protocted. At least that is what I understood from the Ktor docs

Mr3zee avatar Aug 21 '24 18:08 Mr3zee

@EricDeng1001 were you able to resolve the issue?

Mr3zee avatar Aug 27 '24 11:08 Mr3zee

I will try to add headers and see. Will report asap.

EricDeng1001 avatar Aug 28 '24 07:08 EricDeng1001

Need newest patch to test it in production env. Waiting for it.

EricDeng1001 avatar Aug 29 '24 13:08 EricDeng1001

why not just pass the access token with the function arguments?

lsafer-meemer avatar Oct 17 '24 10:10 lsafer-meemer

Did anyone find a solution for this? I'm not able to find a way to make my authenticated services to work. In my case I do have this:

// Server
fun Application.routes() = routing {
    authenticate("auth-jwt") {
        rpc("/bire") {
            rpcConfig {
                serialization { json() }
            }

            registerService<TransactionsService> { context ->
                TransactionsRpcService(context)
            }
        }
    }
}
// Client
fun rpcClient(httpClient: HttpClient): suspend () -> RPCClient = {
    httpClient.rpc {
        url("ws://localhost:8080/bire")
        bearerAuth("token") // 1) should this inject the token in the websocket request?
        headers["Authentication"] = "Bearer token" // 2) I also tried this but same result
        rpcConfig {
            waitForServices = true
            serialization {
                json()
            }
        }
    }
}

Screenshot 2025-01-10 at 20 44 34

cc @EricDeng1001 @Mr3zee

Atternatt avatar Jan 10 '25 19:01 Atternatt

Bearer authentication allows passing the token in the url query. That is how I worked around this.

Source https://datatracker.ietf.org/doc/html/rfc6750#section-2.3

Obviously it is needed to by handled manually since Ktor utilizes the Authorization header only

lsafer-meemer avatar Jan 30 '25 05:01 lsafer-meemer

yes I did something similar, thanks @lsafer-meemer 🙏🏻

Atternatt avatar Jan 30 '25 09:01 Atternatt

As op I no longer have time to investigate in this issue, so closing it. Any future related issue should open a new one. And thanks for the author for making efforts in such a wonderful plugin.

EricDeng1001 avatar Feb 14 '25 10:02 EricDeng1001