Skip to content
Original file line number Diff line number Diff line change
@@ -0,0 +1,228 @@
/*
* Copyright 2023-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license.
*/

package kotlinx.rpc.grpc.client

import kotlinx.rpc.grpc.GrpcMetadata
import kotlinx.rpc.grpc.descriptor.MethodDescriptor
import kotlinx.rpc.grpc.plus

/**
* Provides per-call authentication credentials for gRPC calls.
*
* Call credentials are used to attach authentication information (such as tokens, API keys, or signatures)
* to individual gRPC calls through metadata headers. Unlike client credentials, which establish
* the transport security layer (e.g., TLS), call credentials operate at the application layer
* and can be dynamically generated for each request.
*
* ## Usage
*
* Implement this interface to create custom authentication mechanisms:
*
* ```kotlin
* class BearerTokenCredentials(private val token: String) : GrpcCallCredentials {
* override suspend fun Context.getRequestMetadata(): GrpcMetadata {
* return buildGrpcMetadata {
* append("Authorization", "Bearer $token")
* }
* }
* }
* ```
*
* ## Context-Aware Credentials
*
* Use the [Context] to implement sophisticated authentication strategies:
*
* ```kotlin
* class MethodScopedCredentials : GrpcCallCredentials {
* override suspend fun Context.getRequestMetadata(): GrpcMetadata {
* val scope = when (methodName) {
* "GetUser" -> "read:users"
* "UpdateUser" -> "write:users"
* else -> "default"
* }
* val token = fetchTokenWithScope(scope)
* return buildGrpcMetadata {
* append("Authorization", "Bearer $token")
* }
* }
* }
* ```
*
* ## Combining Credentials
*
* Credentials can be combined using the [plus] operator or [combine] function:
*
* ```kotlin
* val credentials = TlsClientCredentials(...) + BearerTokenCredentials("my-token")
* ```
*
* ## Transport Security
*
* By default, call credentials require transport security (TLS) to prevent credential leakage.
* Override [requiresTransportSecurity] to `false` only for testing or non-production environments.
*
* @see getRequestMetadata
* @see Context
* @see requiresTransportSecurity
* @see plus
* @see combine
*/
public interface GrpcCallCredentials {

/**
* Retrieves authentication metadata for the gRPC call.
*
* This method is invoked before each gRPC call to generate authentication headers or metadata.
* Implementations should return a [GrpcMetadata] object containing the necessary authentication
* information for the request.
*
* The method is suspending to allow asynchronous operations such as token retrieval from secure storage.
*
* ## Context Information
*
* The [Context] receiver provides access to call-specific information:
* - [Context.methodName]: The method being invoked (for method-specific auth)
* - [Context.authority]: The target authority (for tenant-aware auth)
*
* ## Examples
*
* Simple bearer token:
* ```kotlin
* override suspend fun Context.getRequestMetadata(): GrpcMetadata {
* return buildGrpcMetadata {
* append("Authorization", "Bearer $token")
* }
* }
* ```
*
* Throwing a [kotlinx.rpc.grpc.StatusException] to fail the call:
* ```kotlin
* override suspend fun Context.getRequestMetadata(): GrpcMetadata {
* val token = try {
* refreshToken()
* } catch (e: Exception) {
* throw StatusException(Status(StatusCode.UNAUTHENTICATED, "Token refresh failed"))
* }
*
* return buildGrpcMetadata {
* append("Authorization", "Bearer $token")
* }
* }
* ```
*
* @receiver Context information about the call being authenticated.
* @return Metadata containing authentication information to attach to the request.
* @throws kotlinx.rpc.grpc.StatusException to abort the call with a specific gRPC status.
*/
public suspend fun Context.getRequestMetadata(): GrpcMetadata

/**
* Indicates whether this credential requires transport security (TLS).
*
* When `true` (the default), the credential will only be applied to calls over secure transports.
* If transport security is not present, the call will fail with [kotlinx.rpc.grpc.StatusCode.UNAUTHENTICATED].
*
* Set to `false` only for credentials that are safe to send over insecure connections,
* such as in testing environments or for non-sensitive authentication mechanisms.
*
* @return `true` if transport security is required, `false` otherwise.
*/
public val requiresTransportSecurity: Boolean
get() = true

/**
* Context information available when retrieving call credentials.
*
* Provides metadata about the RPC call to enable method-specific authentication strategies.
*
* @property methodName The method name of the RPC being invoked.
* @property authority The authority (host:port) for this call.
*/
// TODO: check whether we should add GrpcCallOptions in the context (KRPC-232)
public data class Context(
val authority: String,
val methodName: String,
)
}

/**
* Combines two call credentials into a single credential that applies both.
*
* The resulting credential will apply both sets of credentials in order, allowing
* multiple authentication mechanisms to be used simultaneously. For example,
* combining channel credentials with call credentials, or applying multiple
* authentication headers to the same call.
*
* The combined credential requires transport security if either of the original
* credentials requires it.
*
* ## Example
*
* ```kotlin
* val tlsCreds = TlsClientCredentials { ... }
* val bearerToken = BearerTokenCredentials("my-token")
* val combined = tlsCreds + bearerToken
* ```
*
* Multiple credentials can be chained:
* ```kotlin
* val combined = creds1 + creds2 + creds3
* ```
*
* @param other The credential to combine with this one.
* @return A new credential that applies both credentials.
* @see combine
*/
public operator fun GrpcCallCredentials.plus(other: GrpcCallCredentials): GrpcCallCredentials {
return CombinedCallCredentials(this, other)
}

/**
* Combines two call credentials into a single credential that applies both.
*
* This is an alias for the [plus] operator, providing a more explicit method name
* for combining credentials.
*
* @param other The credential to combine with this one.
* @return A new credential that applies both credentials.
* @see plus
*/
public fun GrpcCallCredentials.combine(other: GrpcCallCredentials): GrpcCallCredentials = this + other

/**
* A call credential that performs no authentication.
*
* This is useful as a no-op placeholder or for disabling authentication in specific scenarios.
* Since it performs no authentication, it does not require transport security.
*
* ## Example
*
* ```kotlin
* val credentials = if (useAuth) {
* BearerTokenCredentials(token)
* } else {
* EmptyCallCredentials
* }
* ```
*/
public object EmptyCallCredentials : GrpcCallCredentials {
override suspend fun GrpcCallCredentials.Context.getRequestMetadata(): GrpcMetadata {
return GrpcMetadata()
}
override val requiresTransportSecurity: Boolean = false
}

internal class CombinedCallCredentials(
private val first: GrpcCallCredentials,
private val second: GrpcCallCredentials
) : GrpcCallCredentials {
override suspend fun GrpcCallCredentials.Context.getRequestMetadata(): GrpcMetadata {
val firstMetadata = with(first) { getRequestMetadata() }
val secondMetadata = with(second) { getRequestMetadata() }
return firstMetadata + secondMetadata
}

override val requiresTransportSecurity: Boolean = first.requiresTransportSecurity || second.requiresTransportSecurity
}
Original file line number Diff line number Diff line change
Expand Up @@ -49,4 +49,49 @@ public class GrpcCallOptions {
* @see GrpcCompression
*/
public var compression: GrpcCompression = GrpcCompression.None

/**
* Per-call authentication credentials to apply to this RPC.
*
* Call credentials allow dynamic, per-request authentication by adding metadata headers
* such as bearer tokens, API keys, or custom authentication information.
*
* ## Default Behavior
* Defaults to [EmptyCallCredentials], which attaches no authentication headers.
*
* ## Usage Patterns
*
* ### Setting in Client Interceptors
* Call credentials can be dynamically added or modified in client interceptors:
*
* ```kotlin
* val client = GrpcClient("example.com", 443) {
* interceptors {
* clientInterceptor {
* callOptions.callCredentials += BearerTokenCredentials(getToken())
* proceed(it)
* }
* }
* }
* ```
*
* ### Combining Multiple Credentials
* Multiple call credentials can be combined using the `+` operator:
*
* ```kotlin
* callOptions.callCredentials = bearerToken + apiKey
* // or incrementally:
* callOptions.callCredentials += additionalCredential
* ```
*
* ### Transport Security
* If any call credential requires transport security ([GrpcCallCredentials.requiresTransportSecurity]),
* the call will fail with [kotlinx.rpc.grpc.StatusCode.UNAUTHENTICATED] unless the channel
* is configured with TLS credentials (which is the default, except if the client uses `plaintext()`).
*
* @see GrpcCallCredentials
* @see EmptyCallCredentials
* @see GrpcCallCredentials.plus
*/
public var callCredentials: GrpcCallCredentials = EmptyCallCredentials
}
Original file line number Diff line number Diff line change
Expand Up @@ -33,13 +33,13 @@ private typealias RequestClient = Any

/**
* GrpcClient manages gRPC communication by providing implementation for making asynchronous RPC calls.
*
* @field channel The [kotlinx.rpc.grpc.client.internal.ManagedChannel] used to communicate with remote gRPC services.
*/
public class GrpcClient internal constructor(
internal val channel: ManagedChannel,
messageCodecResolver: MessageCodecResolver = EmptyMessageCodecResolver,
internal val interceptors: List<ClientInterceptor>,
// the default call credentials that are automatically attached to all calls made with this client
internal val callCredentials: GrpcCallCredentials,
) : RpcClient {
private val delegates = RpcInternalConcurrentHashMap<String, GrpcServiceDelegate>()
private val messageCodecResolver = messageCodecResolver + ThrowingMessageCodecResolver
Expand Down Expand Up @@ -182,7 +182,8 @@ private fun GrpcClient(
config: GrpcClientConfiguration,
): GrpcClient {
val channel = builder.applyConfig(config).buildChannel()
return GrpcClient(channel, config.messageCodecResolver, config.interceptors)
val callCredentials = config.credentials?.realCallCredentials ?: EmptyCallCredentials
return GrpcClient(channel, config.messageCodecResolver, config.interceptors, callCredentials)
}


Expand Down Expand Up @@ -234,7 +235,7 @@ public class GrpcClientConfiguration internal constructor() {
* @see tls
* @see plaintext
*/
public var credentials: ClientCredentials? = null
public var credentials: GrpcClientCredentials? = null

/**
* Overrides the authority used with TLS and HTTP virtual hosting.
Expand Down Expand Up @@ -272,7 +273,7 @@ public class GrpcClientConfiguration internal constructor() {
*
* @return An insecure [ClientCredentials] instance that must be passed to [credentials].
*/
public fun plaintext(): ClientCredentials = createInsecureClientCredentials()
public fun plaintext(): GrpcClientCredentials = GrpcInsecureClientCredentials()

/**
* Configures and creates secure client credentials for the gRPC client.
Expand All @@ -286,13 +287,13 @@ public class GrpcClientConfiguration internal constructor() {
* Alternatively, you can use the [TlsClientCredentials] constructor.
*
* @param configure A configuration block that allows setting up the TLS parameters
* using the [TlsClientCredentialsBuilder].
* using the [GrpcTlsClientCredentialsBuilder].
* @return A secure [ClientCredentials] instance that must be passed to [credentials].
*
* @see credentials
*/
public fun tls(configure: TlsClientCredentialsBuilder.() -> Unit): ClientCredentials =
TlsClientCredentials(configure)
public fun tls(configure: GrpcTlsClientCredentialsBuilder.() -> Unit): GrpcClientCredentials =
GrpcTlsClientCredentials(configure)

/**
* Configures keep-alive settings for the gRPC client.
Expand Down
Loading
Loading