1+ /*
2+ * Copyright 2023-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license.
3+ */
4+
5+ package kotlinx.rpc.grpc.test.proto
6+
7+ import kotlinx.rpc.RpcServer
8+ import kotlinx.rpc.grpc.GrpcMetadata
9+ import kotlinx.rpc.grpc.Status
10+ import kotlinx.rpc.grpc.StatusCode
11+ import kotlinx.rpc.grpc.StatusException
12+ import kotlinx.rpc.grpc.append
13+ import kotlinx.rpc.grpc.client.GrpcCallCredentials
14+ import kotlinx.rpc.grpc.client.GrpcCallOptions
15+ import kotlinx.rpc.grpc.client.GrpcClient
16+ import kotlinx.rpc.grpc.client.TlsClientCredentials
17+ import kotlinx.rpc.grpc.client.plus
18+ import kotlinx.rpc.grpc.getAll
19+ import kotlinx.rpc.grpc.server.TlsServerCredentials
20+ import kotlinx.rpc.grpc.test.EchoRequest
21+ import kotlinx.rpc.grpc.test.EchoService
22+ import kotlinx.rpc.grpc.test.EchoServiceImpl
23+ import kotlinx.rpc.grpc.test.SERVER_CERT_PEM
24+ import kotlinx.rpc.grpc.test.SERVER_KEY_PEM
25+ import kotlinx.rpc.grpc.test.assertGrpcFailure
26+ import kotlinx.rpc.grpc.test.invoke
27+ import kotlinx.rpc.registerService
28+ import kotlinx.rpc.withService
29+ import kotlin.test.Test
30+ import kotlin.test.assertEquals
31+
32+ /* *
33+ * Tests that the client can configure the compression of requests.
34+ *
35+ * This test is hard to realize on native, as the gRPC-Core doesn't expose internal headers like
36+ * `grpc-encoding` to the user application. This means we cannot verify that the client or sever
37+ * actually sent those headers on native. Instead, we capture the grpc trace output (written to stderr)
38+ * and verify that the client and server actually used the compression algorithm.
39+ */
40+ class GrpcCallCredentialsTest : GrpcProtoTest () {
41+ override fun RpcServer.registerServices () {
42+ return registerService<EchoService > { EchoServiceImpl () }
43+ }
44+
45+ @Test
46+ fun `test simple combined call credentials - should succeed` () {
47+ var grpcMetadata: GrpcMetadata ? = null
48+ runGrpcTest(
49+ configure = {
50+ credentials = plaintext() + NoTLSBearerTokenCredentials ()
51+ },
52+ serverInterceptors = serverInterceptor {
53+ grpcMetadata = requestHeaders
54+ proceed(it)
55+ },
56+ test = ::unaryCall
57+ )
58+
59+ val authHeaders = grpcMetadata?.getAll(" authorization" )
60+ assertEquals(1 , authHeaders?.size)
61+ assertEquals(" Bearer token" , authHeaders?.single())
62+ }
63+
64+ @Test
65+ fun `test combine multiple call credentials - should succeed` () {
66+ var grpcMetadata: GrpcMetadata ? = null
67+ val callCreds = (NoTLSBearerTokenCredentials (" token-1" ) + NoTLSBearerTokenCredentials (" token-2" ))
68+ runGrpcTest(
69+ configure = {
70+ credentials = plaintext() + callCreds
71+ },
72+ serverInterceptors = serverInterceptor {
73+ grpcMetadata = requestHeaders
74+ proceed(it)
75+ },
76+ test = ::unaryCall
77+ )
78+ val authHeaders = grpcMetadata?.getAll(" authorization" )
79+ assertEquals(2 , authHeaders?.size)
80+ assertEquals(" Bearer token-1" , authHeaders?.first())
81+ assertEquals(" Bearer token-2" , authHeaders?.get(1 ))
82+ }
83+
84+ @Test
85+ fun `test plaintext call credentials - should fail` () {
86+ assertGrpcFailure(StatusCode .UNAUTHENTICATED , " Transport security required but not present" ) {
87+ runGrpcTest(
88+ configure = {
89+ credentials = plaintext() + TlsBearerTokenCredentials ()
90+ },
91+ test = ::unaryCall
92+ )
93+ }
94+ }
95+
96+ @Test
97+ fun `test tls call credentials - should succeed` () {
98+ val serverTls = TlsServerCredentials (SERVER_CERT_PEM , SERVER_KEY_PEM )
99+ val clientTls = TlsClientCredentials { trustManager(SERVER_CERT_PEM ) }
100+ val clientCombined = clientTls + TlsBearerTokenCredentials ()
101+
102+ var grpcMetadata: GrpcMetadata ? = null
103+ runGrpcTest(
104+ configure = {
105+ credentials = clientCombined
106+ overrideAuthority = " foo.test.google.fr"
107+ },
108+ serverCreds = serverTls,
109+ serverInterceptors = serverInterceptor {
110+ grpcMetadata = requestHeaders
111+ proceed(it)
112+ },
113+ test = ::unaryCall
114+ )
115+
116+ val authHeaders = grpcMetadata?.getAll(" authorization" )
117+ assertEquals(1 , authHeaders?.size)
118+ assertEquals(" Bearer token" , authHeaders?.single())
119+ }
120+
121+ @Test
122+ fun `test throw status exception - should fail with status` () {
123+ val throwingCallCredentials = object : GrpcCallCredentials {
124+ override suspend fun GrpcMetadata.applyOnMetadata (callOptions : GrpcCallOptions ) {
125+ throw StatusException (Status (StatusCode .UNIMPLEMENTED , " This is my custom exception" ))
126+ }
127+
128+ override val requiresTransportSecurity: Boolean
129+ get() = false
130+ }
131+
132+ assertGrpcFailure(StatusCode .UNIMPLEMENTED , " This is my custom exception" ) {
133+ runGrpcTest(
134+ configure = {
135+ credentials = plaintext() + throwingCallCredentials
136+ },
137+ serverInterceptors = serverInterceptor {
138+ proceed(it)
139+ },
140+ test = ::unaryCall
141+ )
142+ }
143+ }
144+
145+ @Test
146+ fun `test interceptor call credentials - should succeed` () {
147+ var grpcMetadata: GrpcMetadata ? = null
148+ runGrpcTest(
149+ clientInterceptors = clientInterceptor {
150+ callOptions.callCredentials + = NoTLSBearerTokenCredentials ()
151+ proceed(it)
152+ },
153+ serverInterceptors = serverInterceptor {
154+ grpcMetadata = requestHeaders
155+ proceed(it)
156+ },
157+ test = ::unaryCall
158+ )
159+ val authHeaders = grpcMetadata?.getAll(" authorization" )
160+ assertEquals(1 , authHeaders?.size)
161+ assertEquals(" Bearer token" , authHeaders?.single())
162+ }
163+
164+ @Test
165+ fun `test interceptor call credentials without TLS - should fail` () {
166+ assertGrpcFailure(StatusCode .UNAUTHENTICATED , " Transport security required but not present" ) {
167+ runGrpcTest(
168+ clientInterceptors = clientInterceptor {
169+ callOptions.callCredentials + = TlsBearerTokenCredentials ()
170+ proceed(it)
171+ },
172+ test = ::unaryCall
173+ )}
174+ }
175+ }
176+
177+ private suspend fun unaryCall (grpcClient : GrpcClient ) {
178+ val service = grpcClient.withService<EchoService >()
179+ val response = service.UnaryEcho (EchoRequest { message = " Echo" })
180+ assertEquals(" Echo" , response.message)
181+ }
182+
183+ class NoTLSBearerTokenCredentials (
184+ val token : String = " token"
185+ ): GrpcCallCredentials {
186+ override suspend fun GrpcMetadata.applyOnMetadata (callOptions : GrpcCallOptions ) {
187+ // potentially fetching the token from a secure storage
188+ append(" Authorization" , " Bearer $token " )
189+ }
190+
191+ override val requiresTransportSecurity: Boolean
192+ get() = false
193+ }
194+
195+ class TlsBearerTokenCredentials : GrpcCallCredentials {
196+ override suspend fun GrpcMetadata.applyOnMetadata (callOptions : GrpcCallOptions ) {
197+ append(" Authorization" , " Bearer token" )
198+ }
199+ }
0 commit comments