Skip to content

Commit dc2a76a

Browse files
committed
grpc: Update API
1 parent c258a39 commit dc2a76a

File tree

4 files changed

+162
-40
lines changed

4 files changed

+162
-40
lines changed

grpc/grpc-client/src/commonMain/kotlin/kotlinx/rpc/grpc/client/GrpcCallCredentials.kt

Lines changed: 83 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
package kotlinx.rpc.grpc.client
66

77
import kotlinx.rpc.grpc.GrpcMetadata
8+
import kotlinx.rpc.grpc.descriptor.MethodDescriptor
9+
import kotlinx.rpc.grpc.plus
810

911
/**
1012
* Provides per-call authentication credentials for gRPC calls.
@@ -20,12 +22,36 @@ import kotlinx.rpc.grpc.GrpcMetadata
2022
*
2123
* ```kotlin
2224
* class BearerTokenCredentials(private val token: String) : GrpcCallCredentials {
23-
* override suspend fun GrpcMetadata.applyOnMetadata(callOptions: GrpcCallOptions) {
24-
* append("Authorization", "Bearer $token")
25+
* override suspend fun Context.getRequestMetadata(): GrpcMetadata {
26+
* return buildGrpcMetadata {
27+
* append("Authorization", "Bearer $token")
28+
* }
2529
* }
2630
* }
2731
* ```
2832
*
33+
* ## Context-Aware Credentials
34+
*
35+
* Use the [Context] to implement sophisticated authentication strategies:
36+
*
37+
* ```kotlin
38+
* class MethodScopedCredentials : GrpcCallCredentials {
39+
* override suspend fun Context.getRequestMetadata(): GrpcMetadata {
40+
* val scope = when (method.name) {
41+
* "GetUser" -> "read:users"
42+
* "UpdateUser" -> "write:users"
43+
* else -> "default"
44+
* }
45+
* val token = fetchTokenWithScope(scope)
46+
* return buildGrpcMetadata {
47+
* append("Authorization", "Bearer $token")
48+
* }
49+
* }
50+
* }
51+
* ```
52+
*
53+
* ## Combining Credentials
54+
*
2955
* Credentials can be combined using the [plus] operator or [combine] function:
3056
*
3157
* ```kotlin
@@ -37,52 +63,70 @@ import kotlinx.rpc.grpc.GrpcMetadata
3763
* By default, call credentials require transport security (TLS) to prevent credential leakage.
3864
* Override [requiresTransportSecurity] to `false` only for testing or non-production environments.
3965
*
40-
* @see applyOnMetadata
66+
* @see getRequestMetadata
67+
* @see Context
4168
* @see requiresTransportSecurity
4269
* @see plus
4370
* @see combine
4471
*/
4572
public interface GrpcCallCredentials {
4673

4774
/**
48-
* Applies authentication metadata to the gRPC call.
75+
* Retrieves authentication metadata for the gRPC call.
76+
*
77+
* This method is invoked before each gRPC call to generate authentication headers or metadata.
78+
* Implementations should return a [GrpcMetadata] object containing the necessary authentication
79+
* information for the request.
4980
*
50-
* This method is invoked before each gRPC call to add authentication headers or metadata.
51-
* Implementations should append the necessary authentication information to the [GrpcMetadata] receiver.
81+
* The method is suspending to allow asynchronous operations such as:
82+
* - Token retrieval from secure storage
83+
* - OAuth token refresh or exchange
84+
* - Dynamic token generation or signing
85+
* - Network calls to authentication services
5286
*
53-
* The method is suspending to allow asynchronous token retrieval or refresh operations,
54-
* such as fetching tokens from secure storage or performing OAuth token exchanges.
87+
* ## Context Information
88+
*
89+
* The [Context] receiver provides access to call-specific information:
90+
* - [Context.method]: The method being invoked (for method-specific auth)
91+
* - [Context.authority]: The target authority (for tenant-aware auth)
5592
*
5693
* ## Examples
5794
*
58-
* Adding a bearer token:
95+
* Simple bearer token:
5996
* ```kotlin
60-
* override suspend fun GrpcMetadata.applyOnMetadata(callOptions: GrpcCallOptions) {
61-
* append("Authorization", "Bearer $token")
97+
* override suspend fun Context.getRequestMetadata(): GrpcMetadata {
98+
* return buildGrpcMetadata {
99+
* append("Authorization", "Bearer $token")
100+
* }
62101
* }
63102
* ```
64103
*
65104
* Throwing a [kotlinx.rpc.grpc.StatusException] to fail the call:
66105
* ```kotlin
67-
* override suspend fun GrpcMetadata.applyOnMetadata(callOptions: GrpcCallOptions) {
68-
* if (!isValid) {
69-
* throw StatusException(Status(StatusCode.UNAUTHENTICATED, "Invalid credentials"))
106+
* override suspend fun Context.getRequestMetadata(): GrpcMetadata {
107+
* val token = try {
108+
* refreshToken()
109+
* } catch (e: Exception) {
110+
* throw StatusException(Status(StatusCode.UNAUTHENTICATED, "Token refresh failed"))
111+
* }
112+
*
113+
* return buildGrpcMetadata {
114+
* append("Authorization", "Bearer $token")
70115
* }
71-
* append("Authorization", "Bearer $token")
72116
* }
73117
* ```
74118
*
75-
* @param callOptions The options for the current call, providing context and configuration.
76-
* @receiver The metadata to which authentication information should be added.
119+
* @receiver Context information about the call being authenticated.
120+
* @return Metadata containing authentication information to attach to the request.
77121
* @throws kotlinx.rpc.grpc.StatusException to abort the call with a specific gRPC status.
78122
*/
79-
public suspend fun GrpcMetadata.applyOnMetadata(callOptions: GrpcCallOptions)
123+
public suspend fun Context.getRequestMetadata(): GrpcMetadata
80124

81125
/**
82126
* Indicates whether this credential requires transport security (TLS).
83127
*
84128
* When `true` (the default), the credential will only be applied to calls over secure transports.
85-
* If transport security is not present, the call will fail with `UNAUTHENTICATED`.
129+
* If transport security is not present, the call will fail with [kotlinx.rpc.grpc.StatusCode.UNAUTHENTICATED].
86130
*
87131
* Set to `false` only for credentials that are safe to send over insecure connections,
88132
* such as in testing environments or for non-sensitive authentication mechanisms.
@@ -91,6 +135,20 @@ public interface GrpcCallCredentials {
91135
*/
92136
public val requiresTransportSecurity: Boolean
93137
get() = true
138+
139+
/**
140+
* Context information available when retrieving call credentials.
141+
*
142+
* Provides metadata about the RPC call to enable method-specific authentication strategies.
143+
*
144+
* @property method The method descriptor of the RPC being invoked.
145+
* @property authority The authority (host:port) for this call.
146+
*/
147+
// TODO: check whether we should add GrpcCallOptions in the context (KRPC-232)
148+
public data class Context(
149+
val method: MethodDescriptor<*, *>,
150+
val authority: String,
151+
)
94152
}
95153

96154
/**
@@ -154,8 +212,8 @@ public fun GrpcCallCredentials.combine(other: GrpcCallCredentials): GrpcCallCred
154212
* ```
155213
*/
156214
public object EmptyCallCredentials : GrpcCallCredentials {
157-
override suspend fun GrpcMetadata.applyOnMetadata(callOptions: GrpcCallOptions) {
158-
// do nothing
215+
override suspend fun GrpcCallCredentials.Context.getRequestMetadata(): GrpcMetadata {
216+
return GrpcMetadata()
159217
}
160218
override val requiresTransportSecurity: Boolean = false
161219
}
@@ -164,15 +222,10 @@ internal class CombinedCallCredentials(
164222
private val first: GrpcCallCredentials,
165223
private val second: GrpcCallCredentials
166224
) : GrpcCallCredentials {
167-
override suspend fun GrpcMetadata.applyOnMetadata(
168-
callOptions: GrpcCallOptions
169-
) {
170-
with(first) {
171-
this@applyOnMetadata.applyOnMetadata(callOptions)
172-
}
173-
with(second) {
174-
this@applyOnMetadata.applyOnMetadata(callOptions)
175-
}
225+
override suspend fun GrpcCallCredentials.Context.getRequestMetadata(): GrpcMetadata {
226+
val firstMetadata = with(first) { getRequestMetadata() }
227+
val secondMetadata = with(second) { getRequestMetadata() }
228+
return firstMetadata + secondMetadata
176229
}
177230

178231
override val requiresTransportSecurity: Boolean = first.requiresTransportSecurity || second.requiresTransportSecurity

grpc/grpc-client/src/jvmMain/kotlin/kotlinx/rpc/grpc/client/credentials.jvm.kt

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ import io.grpc.CallCredentials
88
import io.grpc.ChannelCredentials
99
import io.grpc.CompositeChannelCredentials
1010
import io.grpc.InsecureChannelCredentials
11-
import io.grpc.Metadata
1211
import io.grpc.SecurityLevel
1312
import io.grpc.TlsChannelCredentials
1413
import kotlinx.coroutines.CoroutineScope
@@ -73,8 +72,8 @@ internal fun GrpcCallCredentials.toJvm(): CallCredentials {
7372
"Transport security required but not present"
7473
}
7574

76-
val metadata = Metadata()
77-
metadata.applyOnMetadata(GrpcCallOptions(/* populate from requestInfo if needed */))
75+
val context = GrpcCallCredentials.Context(requestInfo.methodDescriptor, requestInfo.authority)
76+
val metadata = context.getRequestMetadata()
7877
applier.apply(metadata)
7978
} catch (e: StatusException) {
8079
applier.fail(e.status)

grpc/grpc-core/src/commonMain/kotlin/kotlinx/rpc/grpc/GrpcMetadata.kt

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,16 @@ import kotlinx.rpc.grpc.codec.MessageCodec
5151
@Suppress("RedundantConstructorKeyword")
5252
public expect class GrpcMetadata constructor()
5353

54+
/**
55+
* Constructs and configures a new [GrpcMetadata] instance.
56+
* The provided [block] is executed to apply custom modifications to the metadata object.
57+
*
58+
* @param block A lambda function allowing customization of the [GrpcMetadata] object.
59+
* The lambda operates on the metadata instance being built.
60+
* @return The configured [GrpcMetadata] instance.
61+
*/
62+
public fun buildGrpcMetadata(block: GrpcMetadata.() -> Unit): GrpcMetadata = GrpcMetadata().apply(block)
63+
5464
/**
5565
* A typed key for metadata entries that uses a [MessageCodec] to encode and decode values.
5666
*

grpc/grpc-core/src/commonTest/kotlin/kotlinx/rpc/grpc/test/proto/CallCredentialsTest.kt renamed to grpc/grpc-core/src/commonTest/kotlin/kotlinx/rpc/grpc/test/proto/GrpcCallCredentialsTest.kt

Lines changed: 67 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,20 @@ import kotlinx.rpc.grpc.Status
1010
import kotlinx.rpc.grpc.StatusCode
1111
import kotlinx.rpc.grpc.StatusException
1212
import kotlinx.rpc.grpc.append
13+
import kotlinx.rpc.grpc.buildGrpcMetadata
14+
import kotlinx.rpc.grpc.client.BearerTokenCredentials
1315
import kotlinx.rpc.grpc.client.GrpcCallCredentials
14-
import kotlinx.rpc.grpc.client.GrpcCallOptions
16+
import kotlinx.rpc.grpc.client.GrpcCallCredentials.Context
1517
import kotlinx.rpc.grpc.client.GrpcClient
18+
import kotlinx.rpc.grpc.client.JwtCredentials
1619
import kotlinx.rpc.grpc.client.TlsClientCredentials
1720
import kotlinx.rpc.grpc.client.plus
1821
import kotlinx.rpc.grpc.getAll
22+
import kotlin.io.encoding.Base64
23+
import kotlin.io.encoding.ExperimentalEncodingApi
24+
import kotlin.time.Duration.Companion.milliseconds
25+
import kotlin.time.Duration.Companion.seconds
26+
import kotlin.time.TimeSource
1927
import kotlinx.rpc.grpc.server.TlsServerCredentials
2028
import kotlinx.rpc.grpc.test.EchoRequest
2129
import kotlinx.rpc.grpc.test.EchoService
@@ -113,7 +121,7 @@ class GrpcCallCredentialsTest : GrpcProtoTest() {
113121
@Test
114122
fun `test throw status exception - should fail with status`() {
115123
val throwingCallCredentials = object : GrpcCallCredentials {
116-
override suspend fun GrpcMetadata.applyOnMetadata(callOptions: GrpcCallOptions) {
124+
override suspend fun Context.getRequestMetadata(): GrpcMetadata {
117125
throw StatusException(Status(StatusCode.UNIMPLEMENTED, "This is my custom exception"))
118126
}
119127

@@ -164,6 +172,54 @@ class GrpcCallCredentialsTest : GrpcProtoTest() {
164172
test = ::unaryCall
165173
)}
166174
}
175+
176+
@Test
177+
fun `test context contains correct method descriptor - should succeed`() {
178+
var capturedMethod: String? = null
179+
180+
val contextCapturingCredentials = object : GrpcCallCredentials {
181+
override suspend fun Context.getRequestMetadata(): GrpcMetadata {
182+
println(authority)
183+
capturedMethod = method.getFullMethodName()
184+
return GrpcMetadata()
185+
}
186+
187+
override val requiresTransportSecurity: Boolean = false
188+
}
189+
190+
runGrpcTest(
191+
configure = {
192+
credentials = plaintext() + contextCapturingCredentials
193+
},
194+
test = ::unaryCall
195+
)
196+
197+
assertEquals("kotlinx.rpc.grpc.test.EchoService/UnaryEcho", capturedMethod)
198+
}
199+
200+
@Test
201+
fun `test context contains correct authority - should succeed`() {
202+
var capturedAuthority: String? = null
203+
204+
val contextCapturingCredentials = object : GrpcCallCredentials {
205+
override suspend fun Context.getRequestMetadata(): GrpcMetadata {
206+
capturedAuthority = authority
207+
return GrpcMetadata()
208+
}
209+
210+
override val requiresTransportSecurity: Boolean = false
211+
}
212+
213+
runGrpcTest(
214+
configure = {
215+
credentials = plaintext() + contextCapturingCredentials
216+
overrideAuthority = "test.example.com"
217+
},
218+
test = ::unaryCall
219+
)
220+
221+
assertEquals("test.example.com", capturedAuthority)
222+
}
167223
}
168224

169225
private suspend fun unaryCall(grpcClient: GrpcClient) {
@@ -175,17 +231,21 @@ private suspend fun unaryCall(grpcClient: GrpcClient) {
175231
class NoTLSBearerTokenCredentials(
176232
val token: String = "token"
177233
): GrpcCallCredentials {
178-
override suspend fun GrpcMetadata.applyOnMetadata(callOptions: GrpcCallOptions) {
179-
// potentially fetching the token from a secure storage
180-
append("Authorization", "Bearer $token")
234+
override suspend fun Context.getRequestMetadata(): GrpcMetadata {
235+
return buildGrpcMetadata {
236+
// potentially fetching the token from a secure storage
237+
append("Authorization", "Bearer $token")
238+
}
181239
}
182240

183241
override val requiresTransportSecurity: Boolean
184242
get() = false
185243
}
186244

187245
class TlsBearerTokenCredentials: GrpcCallCredentials {
188-
override suspend fun GrpcMetadata.applyOnMetadata(callOptions: GrpcCallOptions) {
189-
append("Authorization", "Bearer token")
246+
override suspend fun Context.getRequestMetadata(): GrpcMetadata {
247+
return buildGrpcMetadata {
248+
append("Authorization", "Bearer token")
249+
}
190250
}
191251
}

0 commit comments

Comments
 (0)