Skip to content

Commit 1cd6714

Browse files
authored
Examples + Docs for App API Token authentication for gRPC and HTTP (#1589)
* example Signed-off-by: Cassandra Coyle <[email protected]> * docs for example Signed-off-by: Cassandra Coyle <[email protected]> --------- Signed-off-by: Cassandra Coyle <[email protected]>
1 parent dce06a8 commit 1cd6714

File tree

7 files changed

+283
-4
lines changed

7 files changed

+283
-4
lines changed

daprdocs/content/en/java-sdk-docs/java-client/_index.md

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -632,6 +632,124 @@ try (DaprClient client = new DaprClientBuilder().build()) {
632632

633633
Learn more about the [Dapr Java SDK packages available to add to your Java applications](https://dapr.github.io/java-sdk/).
634634

635+
## Security
636+
637+
### App API Token Authentication
638+
639+
The building blocks like pubsub, input bindings, or jobs require Dapr to make incoming calls to your application, you can secure these requests using [Dapr App API Token Authentication]({{% ref app-api-token.md %}}). This ensures that only Dapr can invoke your application's endpoints.
640+
641+
#### Understanding the two tokens
642+
643+
Dapr uses two different tokens for securing communication. See [Properties]({{% ref properties.md %}}) for detailed information about both tokens:
644+
645+
- **`DAPR_API_TOKEN`** (Your app → Dapr sidecar): Automatically handled by the Java SDK when using `DaprClient`
646+
- **`APP_API_TOKEN`** (Dapr → Your app): Requires server-side validation in your application
647+
648+
The examples below show how to implement server-side validation for `APP_API_TOKEN`.
649+
650+
#### Implementing server-side token validation
651+
652+
When using gRPC protocol, implement a server interceptor to capture the metadata.
653+
654+
```java
655+
import io.grpc.Context;
656+
import io.grpc.Contexts;
657+
import io.grpc.Metadata;
658+
import io.grpc.ServerCall;
659+
import io.grpc.ServerCallHandler;
660+
import io.grpc.ServerInterceptor;
661+
662+
public class SubscriberGrpcService extends AppCallbackGrpc.AppCallbackImplBase {
663+
public static final Context.Key<Metadata> METADATA_KEY = Context.key("grpc-metadata");
664+
665+
// gRPC interceptor to capture metadata
666+
public static class MetadataInterceptor implements ServerInterceptor {
667+
@Override
668+
public <ReqT, RespT> ServerCall.Listener<ReqT> interceptCall(
669+
ServerCall<ReqT, RespT> call,
670+
Metadata headers,
671+
ServerCallHandler<ReqT, RespT> next) {
672+
Context contextWithMetadata = Context.current().withValue(METADATA_KEY, headers);
673+
return Contexts.interceptCall(contextWithMetadata, call, headers, next);
674+
}
675+
}
676+
677+
// Your service methods go here...
678+
}
679+
```
680+
681+
Register the interceptor when building your gRPC server:
682+
683+
```java
684+
Server server = ServerBuilder.forPort(port)
685+
.intercept(new SubscriberGrpcService.MetadataInterceptor())
686+
.addService(new SubscriberGrpcService())
687+
.build();
688+
server.start();
689+
```
690+
691+
Then, in your service methods, extract the token from metadata:
692+
693+
```java
694+
@Override
695+
public void onTopicEvent(DaprAppCallbackProtos.TopicEventRequest request,
696+
StreamObserver<DaprAppCallbackProtos.TopicEventResponse> responseObserver) {
697+
try {
698+
// Extract metadata from context
699+
Context context = Context.current();
700+
Metadata metadata = METADATA_KEY.get(context);
701+
702+
if (metadata != null) {
703+
String apiToken = metadata.get(
704+
Metadata.Key.of("dapr-api-token", Metadata.ASCII_STRING_MARSHALLER));
705+
706+
// Validate token accordingly
707+
}
708+
709+
// Process the request
710+
// ...
711+
712+
} catch (Throwable e) {
713+
responseObserver.onError(e);
714+
}
715+
}
716+
```
717+
718+
#### Using with HTTP endpoints
719+
720+
For HTTP-based endpoints, extract the token from the headers:
721+
722+
```java
723+
@RestController
724+
public class SubscriberController {
725+
726+
@PostMapping(path = "/endpoint")
727+
public Mono<Void> handleRequest(
728+
@RequestBody(required = false) byte[] body,
729+
@RequestHeader Map<String, String> headers) {
730+
return Mono.fromRunnable(() -> {
731+
try {
732+
// Extract the token from headers
733+
String apiToken = headers.get("dapr-api-token");
734+
735+
// Validate token accordingly
736+
737+
// Process the request
738+
} catch (Exception e) {
739+
throw new RuntimeException(e);
740+
}
741+
});
742+
}
743+
}
744+
```
745+
746+
#### Examples
747+
748+
For working examples with pubsub, bindings, and jobs:
749+
- [PubSub with App API Token Authentication](https://github.com/dapr/java-sdk/tree/master/examples/src/main/java/io/dapr/examples/pubsub#app-api-token-authentication-optional)
750+
- [Bindings with App API Token Authentication](https://github.com/dapr/java-sdk/tree/master/examples/src/main/java/io/dapr/examples/bindings/http#app-api-token-authentication-optional)
751+
- [Jobs with App API Token Authentication](https://github.com/dapr/java-sdk/tree/master/examples/src/main/java/io/dapr/examples/jobs#app-api-token-authentication-optional)
752+
635753
## Related links
636754
- [Java SDK examples](https://github.com/dapr/java-sdk/tree/master/examples/src/main/java/io/dapr/examples)
637755

daprdocs/content/en/java-sdk-docs/java-client/properties.md

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,11 +32,16 @@ When these variables are set, the client will automatically use them to connect
3232
| `DAPR_GRPC_PORT` | The gRPC port for the Dapr sidecar (legacy, `DAPR_GRPC_ENDPOINT` takes precedence) | `50001` |
3333
| `DAPR_HTTP_PORT` | The HTTP port for the Dapr sidecar (legacy, `DAPR_HTTP_ENDPOINT` takes precedence) | `3500` |
3434

35-
### API Token
35+
### API Tokens
36+
37+
Dapr supports two types of API tokens for securing communication:
3638

3739
| Environment Variable | Description | Default |
3840
|---------------------|-------------|---------|
39-
| `DAPR_API_TOKEN` | API token for authentication between app and Dapr sidecar. This is the same token used by the Dapr runtime for API authentication. For more details, see [Dapr API token authentication](https://docs.dapr.io/operations/security/api-token/) and [Environment variables reference](https://docs.dapr.io/reference/environment/#dapr_api_token). | `null` |
41+
| `DAPR_API_TOKEN` | API token for authenticating requests **from your app to the Dapr sidecar**. The Java SDK automatically includes this token in requests when using `DaprClient`. | `null` |
42+
| `APP_API_TOKEN` | API token for authenticating requests **from Dapr to your app**. When set, Dapr includes this token in the `dapr-api-token` header/metadata when calling your application (for pubsub subscribers, input bindings, or job triggers). Your application must validate this token. | `null` |
43+
44+
For implementation examples, see [App API Token Authentication]({{% ref java-client#app-api-token-authentication %}}). For more details, see [Dapr API token authentication](https://docs.dapr.io/operations/security/api-token/).
4045

4146
### gRPC Configuration
4247

examples/src/main/java/io/dapr/examples/bindings/http/README.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,22 @@ b95e7ad31707 confluentinc/cp-zookeeper:7.4.4 "/etc/confluent/dock…" 5 da
7575
```
7676
Click [here](https://github.com/wurstmeister/kafka-docker) for more information about the kafka broker server.
7777

78+
### App API Token Authentication (Optional)
79+
80+
Dapr supports API token authentication to secure communication between Dapr and your application. When using input bindings, Dapr makes incoming calls to your app, and you can validate these requests using the `APP_API_TOKEN`.
81+
82+
For detailed implementation with gRPC interceptors, see the [PubSub README App API Token Authentication section](../pubsub/README.md#app-api-token-authentication-optional).
83+
84+
For HTTP-based apps, check the `dapr-api-token` header in incoming requests. For more details, see the [Dapr App API Token Authentication documentation](https://docs.dapr.io/operations/security/app-api-token/).
85+
86+
**Quick setup:**
87+
88+
```bash
89+
# Export tokens before running the following `dapr run` commands.
90+
export APP_API_TOKEN="your-app-api-token"
91+
export DAPR_API_TOKEN="your-dapr-api-token"
92+
```
93+
7894
### Running the Input binding sample
7995

8096
The input binding sample uses the Spring Boot´s DaprApplication class for initializing the `InputBindingController`. In `InputBindingExample.java` file, you will find the `InputBindingExample` class and the `main` method. See the code snippet below:

examples/src/main/java/io/dapr/examples/jobs/README.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,22 @@ cd examples
4444

4545
Run `dapr init` to initialize Dapr in Self-Hosted Mode if it's not already initialized.
4646

47+
### App API Token Authentication (Optional)
48+
49+
Dapr supports API token authentication to secure communication between Dapr and your application. When using the Jobs API, Dapr makes incoming calls to your app at job trigger time, and you can validate these requests using the `APP_API_TOKEN`.
50+
51+
For detailed implementation with gRPC interceptors, see the [PubSub README App API Token Authentication section](../pubsub/README.md#app-api-token-authentication-optional).
52+
53+
For more details, see the [Dapr App API Token Authentication documentation](https://docs.dapr.io/operations/security/app-api-token/).
54+
55+
**Quick setup:**
56+
57+
```bash
58+
# Export tokens before running the following `dapr run` commands.
59+
export APP_API_TOKEN="your-app-api-token"
60+
export DAPR_API_TOKEN="your-dapr-api-token"
61+
```
62+
4763
### Running the example
4864

4965
This example uses the Java SDK Dapr client in order to **Schedule and Get** Jobs.

examples/src/main/java/io/dapr/examples/pubsub/README.md

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,88 @@ cd examples
4141

4242
Run `dapr init` to initialize Dapr in Self-Hosted Mode if it's not already initialized.
4343

44+
### App API Token Authentication (Optional)
45+
46+
Dapr supports API token authentication to secure communication between Dapr and your application. This feature is useful for numerous APIs like pubsub, bindings, and jobs building blocks where Dapr makes incoming calls to your app.
47+
48+
For more details, see the [Dapr App API Token Authentication documentation](https://docs.dapr.io/operations/security/app-api-token/).
49+
50+
#### How it works
51+
52+
When `APP_API_TOKEN` is set, Dapr includes the token in the gRPC metadata header `dapr-api-token` when calling your app. Your app can validate this token to authenticate requests from Dapr.
53+
54+
#### Setting up tokens
55+
56+
Set a dapr annotation or simply export the environment variables before running your Dapr applications:
57+
58+
```bash
59+
# Token for your app to authenticate requests FROM Dapr
60+
export APP_API_TOKEN="your-app-api-token"
61+
62+
# Token for Dapr client to authenticate requests TO Dapr sidecar
63+
export DAPR_API_TOKEN="your-dapr-api-token"
64+
```
65+
66+
#### Using with gRPC Subscriber
67+
68+
The gRPC subscriber example includes a `MetadataInterceptor` (see `SubscriberGrpcService.java`) that captures the `dapr-api-token` from incoming requests:
69+
70+
```java
71+
public class SubscriberGrpcService extends AppCallbackGrpc.AppCallbackImplBase {
72+
public static final Context.Key<Metadata> METADATA_KEY = Context.key("grpc-metadata");
73+
74+
// gRPC interceptor to capture metadata
75+
public static class MetadataInterceptor implements ServerInterceptor {
76+
@Override
77+
public <ReqT, RespT> ServerCall.Listener<ReqT> interceptCall(
78+
ServerCall<ReqT, RespT> call, Metadata headers, ServerCallHandler<ReqT, RespT> next) {
79+
Context contextWithMetadata = Context.current().withValue(METADATA_KEY, headers);
80+
return Contexts.interceptCall(contextWithMetadata, call, headers, next);
81+
}
82+
}
83+
}
84+
```
85+
86+
Then in your service methods, you can extract and validate the token:
87+
88+
```java
89+
Context context = Context.current();
90+
Metadata metadata = METADATA_KEY.get(context);
91+
String apiToken = metadata.get(Metadata.Key.of("dapr-api-token", Metadata.ASCII_STRING_MARSHALLER));
92+
93+
// Validate token accordingly
94+
```
95+
96+
#### Using with HTTP Subscriber
97+
98+
For HTTP-based endpoints, extract the token from the headers:
99+
100+
```java
101+
@RestController
102+
public class SubscriberController {
103+
104+
@PostMapping(path = "/endpoint")
105+
public Mono<Void> handleRequest(
106+
@RequestBody(required = false) byte[] body,
107+
@RequestHeader Map<String, String> headers) {
108+
return Mono.fromRunnable(() -> {
109+
try {
110+
// Extract the token from headers
111+
String apiToken = headers.get("dapr-api-token");
112+
113+
// Validate token accordingly
114+
115+
// Process the request
116+
} catch (Exception e) {
117+
throw new RuntimeException(e);
118+
}
119+
});
120+
}
121+
}
122+
```
123+
124+
Then use the standard `dapr run` commands shown in the sections below. The subscriber will validate incoming requests from Dapr using `APP_API_TOKEN`, and both applications will authenticate to Dapr using `DAPR_API_TOKEN`.
125+
44126
### Running the publisher
45127

46128
The publisher is a simple Java application with a main method that uses the Dapr gRPC Client to publish 10 messages to a specific topic.

examples/src/main/java/io/dapr/examples/pubsub/grpc/Subscriber.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,8 +48,9 @@ public static void main(String[] args) throws Exception {
4848
int port = Integer.parseInt(cmd.getOptionValue("port"));
4949

5050
//start a grpc server
51-
Server server = ServerBuilder.forPort(port)
52-
.addService(new SubscriberGrpcService())
51+
Server server = ServerBuilder.forPort(port)
52+
.intercept(new SubscriberGrpcService.MetadataInterceptor())
53+
.addService(new SubscriberGrpcService())
5354
.addService(new BulkSubscriberGrpcService())
5455
.build();
5556
server.start();

examples/src/main/java/io/dapr/examples/pubsub/grpc/SubscriberGrpcService.java

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,12 @@
1616
import com.google.protobuf.Empty;
1717
import io.dapr.v1.AppCallbackGrpc;
1818
import io.dapr.v1.DaprAppCallbackProtos;
19+
import io.grpc.Context;
20+
import io.grpc.Contexts;
21+
import io.grpc.Metadata;
22+
import io.grpc.ServerCall;
23+
import io.grpc.ServerCallHandler;
24+
import io.grpc.ServerInterceptor;
1925
import io.grpc.stub.StreamObserver;
2026

2127
import java.util.ArrayList;
@@ -27,6 +33,17 @@
2733
public class SubscriberGrpcService extends AppCallbackGrpc.AppCallbackImplBase {
2834
private final List<DaprAppCallbackProtos.TopicSubscription> topicSubscriptionList = new ArrayList<>();
2935

36+
public static final Context.Key<Metadata> METADATA_KEY = Context.key("grpc-metadata");
37+
// gRPC interceptor to capture metadata
38+
public static class MetadataInterceptor implements ServerInterceptor {
39+
@Override
40+
public <ReqT, RespT> ServerCall.Listener<ReqT> interceptCall(
41+
ServerCall<ReqT, RespT> call, Metadata headers, ServerCallHandler<ReqT, RespT> next) {
42+
Context contextWithMetadata = Context.current().withValue(METADATA_KEY, headers);
43+
return Contexts.interceptCall(contextWithMetadata, call, headers, next);
44+
}
45+
}
46+
3047
@Override
3148
public void listTopicSubscriptions(Empty request,
3249
StreamObserver<DaprAppCallbackProtos.ListTopicSubscriptionsResponse> responseObserver) {
@@ -50,6 +67,30 @@ public void listTopicSubscriptions(Empty request,
5067
public void onTopicEvent(DaprAppCallbackProtos.TopicEventRequest request,
5168
StreamObserver<DaprAppCallbackProtos.TopicEventResponse> responseObserver) {
5269
try {
70+
try {
71+
Context context = Context.current();
72+
Metadata metadata = METADATA_KEY.get(context);
73+
74+
if (metadata != null) {
75+
System.out.println("Metadata found in context");
76+
String apiToken = metadata.get(Metadata.Key.of("dapr-api-token", Metadata.ASCII_STRING_MARSHALLER));
77+
if (apiToken != null) {
78+
System.out.println("API Token extracted: " + apiToken);
79+
} else {
80+
System.out.println("No 'dapr-api-token' found in metadata");
81+
}
82+
System.out.println("All metadata:");
83+
for (String key : metadata.keys()) {
84+
String value = metadata.get(Metadata.Key.of(key, Metadata.ASCII_STRING_MARSHALLER));
85+
System.out.println("key: " + key + ": " + value);
86+
}
87+
} else {
88+
System.out.println("No metadata found in context");
89+
}
90+
} catch (Exception e) {
91+
System.out.println(" Error extracting metadata: " + e.getMessage());
92+
}
93+
5394
String data = request.getData().toStringUtf8().replace("\"", "");
5495
System.out.println("Subscriber got: " + data);
5596
DaprAppCallbackProtos.TopicEventResponse response = DaprAppCallbackProtos.TopicEventResponse.newBuilder()

0 commit comments

Comments
 (0)