Skip to content

Commit 5cb456c

Browse files
committed
feat: Add a sample that shows how to configure A2A client and A2A server security for all three transports
1 parent 834c1e7 commit 5cb456c

File tree

21 files changed

+1388
-0
lines changed

21 files changed

+1388
-0
lines changed

samples/java/agents/README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,9 @@ Each agent can be run as its own A2A server with the instructions in its README.
1919
Sample agent that can roll dice of different sizes and check if numbers are prime. This agent demonstrates
2020
multi-transport capabilities.
2121

22+
* [**Magic 8 Ball Agent (Security)**](magic_8_ball_security/README.md)
23+
Sample agent that can respond to yes/no questions by consulting a Magic 8 Ball. This sample demonstrates how to secure an A2A server with Keycloak using bearer token authentication and it shows how to configure an A2A client to specify the token when sending requests.
24+
2225
## Disclaimer
2326

2427
Important: The sample code provided is for demonstration purposes and illustrates the
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
# Magic 8-Ball Security Agent
2+
3+
This sample agent responds to yes/no questions by consulting a Magic 8-Ball.
4+
5+
This sample demonstrates how to secure an A2A server with Keycloak using bearer token authentication and it shows how to configure an A2A client to specify the token when
6+
sending requests. The agent is written using Quarkus LangChain4j and makes use of the
7+
[A2A Java](https://github.com/a2aproject/a2a-java) SDK.
8+
9+
## Prerequisites
10+
11+
- Java 17 or higher
12+
- Access to an LLM and API Key
13+
- A working container runtime (Docker or [Podman](https://quarkus.io/guides/podman))
14+
15+
>**NOTE**: We'll be making use of Quarkus Dev Services in this sample to automatically create and configure a Keycloak instance that we'll use as our OAuth2 provider. For more details on using Podman with Quarkus, see this [guide](https://quarkus.io/guides/podman).
16+
17+
## Running the Sample
18+
19+
This sample consists of an A2A server agent, which is in the `server` directory, and an A2A client,
20+
which is in the `client` directory.
21+
22+
### Running the A2A Server Agent
23+
24+
1. Navigate to the `magic-8-ball-security` sample directory:
25+
26+
```bash
27+
cd samples/java/agents/magic-8-ball-security/server
28+
```
29+
30+
2. Set your Google AI Studio API Key as an environment variable:
31+
32+
```bash
33+
export QUARKUS_LANGCHAIN4J_AI_GEMINI_API_KEY=your_api_key_here
34+
```
35+
36+
Alternatively, you can create a `.env` file in the `magic-8-ball-security/server` directory:
37+
38+
```bash
39+
QUARKUS_LANGCHAIN4J_AI_GEMINI_API_KEY=your_api_key_here
40+
```
41+
42+
3. Start the A2A server agent
43+
44+
**NOTE:**
45+
By default, the agent will start on port 11000. To override this, add the `-Dquarkus.http.port=YOUR_PORT`
46+
option at the end of the command below.
47+
48+
```bash
49+
mvn quarkus:dev
50+
```
51+
52+
### Running the A2A Java Client
53+
54+
The Java `TestClient` communicates with the Magic 8-Ball Agent using the A2A Java SDK.
55+
56+
The client supports specifying which transport protocol to use ("jsonrpc", "rest", or "grpc"). By default, it uses JSON-RPC.
57+
58+
1. Make sure you have [JBang installed](https://www.jbang.dev/documentation/jbang/latest/installation.html)
59+
60+
2. Run the client using the JBang script:
61+
```bash
62+
cd samples/java/agents/magic-8-ball-security/client/src/main/java/com/samples/a2a/client
63+
jbang TestClientRunner.java
64+
```
65+
66+
Or specify a custom server URL:
67+
```bash
68+
jbang TestClientRunner.java --server-url http://localhost:11000
69+
```
70+
71+
Or specify a custom message:
72+
```bash
73+
jbang TestClientRunner.java --message "Should I refactor this code?"
74+
```
75+
76+
Or specify a specific transport (jsonrpc, grpc, or rest):
77+
```bash
78+
jbang TestClientRunner.java --transport grpc
79+
```
80+
81+
Or combine multiple options:
82+
```bash
83+
jbang TestClientRunner.java --server-url http://localhost:11000 --message "Will my tests pass?" --transport rest
84+
```
85+
86+
## Expected Client Output
87+
88+
The Java A2A client will:
89+
1. Connect to the Magic 8-Ball agent
90+
2. Fetch the agent card
91+
3. Use the specified transport (JSON-RPC by default, or as specified via --transport option)
92+
4. Send the message "Should I deploy this code on Friday?" (or your custom message)
93+
5. Display the Magic 8-Ball's mystical response from the agent
94+
95+
## Keycloak OAuth2 Authentication
96+
97+
This sample includes a `KeycloakOAuth2CredentialService` that implements the `CredentialService` interface from the A2A Java SDK to retrieve tokens from Keycloak
98+
using Keycloak `AuthzClient`.
99+
100+
## Multi-Transport Support
101+
102+
This sample demonstrates multi-transport capabilities by supporting the JSON-RPC, HTTP+JSON/REST, and gRPC transports. The A2A server agent is configured to use a unified port for all three transports.
103+
104+
## Disclaimer
105+
Important: The sample code provided is for demonstration purposes and illustrates the
106+
mechanics of the Agent-to-Agent (A2A) protocol. When building production applications,
107+
it is critical to treat any agent operating outside of your direct control as a
108+
potentially untrusted entity.
109+
110+
All data received from an external agent—including but not limited to its AgentCard,
111+
messages, artifacts, and task statuses—should be handled as untrusted input. For
112+
example, a malicious agent could provide an AgentCard containing crafted data in its
113+
fields (e.g., description, name, skills.description). If this data is used without
114+
sanitization to construct prompts for a Large Language Model (LLM), it could expose
115+
your application to prompt injection attacks. Failure to properly validate and
116+
sanitize this data before use can introduce security vulnerabilities into your
117+
application.
118+
119+
Developers are responsible for implementing appropriate security measures, such as
120+
input validation and secure handling of credentials to protect their systems and users.
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<project xmlns="http://maven.apache.org/POM/4.0.0"
3+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
4+
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
5+
<modelVersion>4.0.0</modelVersion>
6+
7+
<parent>
8+
<groupId>com.samples.a2a</groupId>
9+
<artifactId>magic-8-ball-security</artifactId>
10+
<version>0.1.0</version>
11+
</parent>
12+
13+
<artifactId>magic-8-ball-security-client</artifactId>
14+
<name>Magic 8-Ball Security Agent Client</name>
15+
<description>A2A Magic 8-Ball Security Agent Test Client</description>
16+
17+
<dependencies>
18+
<dependency>
19+
<groupId>io.github.a2asdk</groupId>
20+
<artifactId>a2a-java-sdk-client</artifactId>
21+
<version>${io.a2a.sdk.version}</version>
22+
</dependency>
23+
<dependency>
24+
<groupId>io.github.a2asdk</groupId>
25+
<artifactId>a2a-java-sdk-client-transport-rest</artifactId>
26+
<version>${io.a2a.sdk.version}</version>
27+
</dependency>
28+
<dependency>
29+
<groupId>io.github.a2asdk</groupId>
30+
<artifactId>a2a-java-sdk-client-transport-grpc</artifactId>
31+
<version>${io.a2a.sdk.version}</version>
32+
</dependency>
33+
<dependency>
34+
<groupId>io.grpc</groupId>
35+
<artifactId>grpc-netty-shaded</artifactId>
36+
<scope>runtime</scope>
37+
</dependency>
38+
<dependency>
39+
<groupId>com.fasterxml.jackson.core</groupId>
40+
<artifactId>jackson-databind</artifactId>
41+
</dependency>
42+
<dependency>
43+
<groupId>com.google.auth</groupId>
44+
<artifactId>google-auth-library-oauth2-http</artifactId>
45+
<version>1.19.0</version>
46+
</dependency>
47+
<dependency>
48+
<groupId>com.google.http-client</groupId>
49+
<artifactId>google-http-client-jackson2</artifactId>
50+
<version>1.43.3</version>
51+
</dependency>
52+
<dependency>
53+
<groupId>org.keycloak</groupId>
54+
<artifactId>keycloak-authz-client</artifactId>
55+
<version>25.0.1</version>
56+
</dependency>
57+
</dependencies>
58+
59+
<build>
60+
<plugins>
61+
<plugin>
62+
<groupId>org.apache.maven.plugins</groupId>
63+
<artifactId>maven-compiler-plugin</artifactId>
64+
</plugin>
65+
<plugin>
66+
<groupId>org.codehaus.mojo</groupId>
67+
<artifactId>exec-maven-plugin</artifactId>
68+
<configuration>
69+
<mainClass>com.samples.a2a.TestClient</mainClass>
70+
</configuration>
71+
</plugin>
72+
</plugins>
73+
</build>
74+
</project>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
package com.samples.a2a.client;
2+
3+
import com.samples.a2a.client.util.CachedToken;
4+
import com.samples.a2a.client.util.KeycloakUtil;
5+
import io.a2a.client.transport.spi.interceptors.ClientCallContext;
6+
import io.a2a.client.transport.spi.interceptors.auth.CredentialService;
7+
import java.util.concurrent.ConcurrentHashMap;
8+
import java.util.concurrent.ConcurrentMap;
9+
import org.keycloak.authorization.client.AuthzClient;
10+
11+
/**
12+
* A CredentialService implementation that provides OAuth2 access tokens
13+
* using Keycloak. This service is used by the A2A client transport
14+
* authentication interceptors.
15+
*/
16+
public final class KeycloakOAuth2CredentialService implements CredentialService {
17+
18+
/** OAuth2 scheme name. */
19+
private static final String OAUTH2_SCHEME_NAME = "oauth2";
20+
21+
/** Token cache. */
22+
private final ConcurrentMap<String, CachedToken> tokenCache
23+
= new ConcurrentHashMap<>();
24+
25+
/** Keycloak authz client. */
26+
private final AuthzClient authzClient;
27+
28+
/**
29+
* Creates a new KeycloakOAuth2CredentialService using the
30+
* default keycloak.json file.
31+
*
32+
* @throws IllegalArgumentException if keycloak.json cannot be found/loaded
33+
*/
34+
public KeycloakOAuth2CredentialService() {
35+
this.authzClient = KeycloakUtil.createAuthzClient();
36+
}
37+
38+
@Override
39+
public String getCredential(final String securitySchemeName,
40+
final ClientCallContext clientCallContext) {
41+
if (!OAUTH2_SCHEME_NAME.equals(securitySchemeName)) {
42+
throw new IllegalArgumentException("Unsupported security scheme: "
43+
+ securitySchemeName);
44+
}
45+
46+
try {
47+
return KeycloakUtil.getAccessToken(securitySchemeName,
48+
tokenCache, authzClient);
49+
} catch (Exception e) {
50+
throw new RuntimeException(
51+
"Failed to obtain OAuth2 access token for scheme: "
52+
+ securitySchemeName, e);
53+
}
54+
}
55+
}
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
package com.samples.a2a.client;
2+
3+
import com.samples.a2a.client.util.EventHandlerUtil;
4+
import io.a2a.client.Client;
5+
import io.a2a.client.ClientEvent;
6+
import io.a2a.client.config.ClientConfig;
7+
import io.a2a.client.transport.grpc.GrpcTransport;
8+
import io.a2a.client.transport.grpc.GrpcTransportConfigBuilder;
9+
import io.a2a.client.transport.jsonrpc.JSONRPCTransport;
10+
import io.a2a.client.transport.jsonrpc.JSONRPCTransportConfigBuilder;
11+
import io.a2a.client.transport.rest.RestTransport;
12+
import io.a2a.client.transport.rest.RestTransportConfigBuilder;
13+
import io.a2a.client.transport.spi.interceptors.auth.AuthInterceptor;
14+
import io.a2a.client.transport.spi.interceptors.auth.CredentialService;
15+
import io.a2a.spec.AgentCard;
16+
import io.grpc.Channel;
17+
import io.grpc.ManagedChannelBuilder;
18+
import java.util.List;
19+
import java.util.concurrent.CompletableFuture;
20+
import java.util.function.BiConsumer;
21+
import java.util.function.Consumer;
22+
import java.util.function.Function;
23+
24+
/**
25+
* Test client utility for creating A2A clients with HTTP-based transports
26+
* and OAuth2 authentication.
27+
*
28+
* <p>This class encapsulates the complexity of setting up A2A clients with
29+
* multiple transport options (gRPC, REST, JSON-RPC) and Keycloak OAuth2
30+
* authentication, providing simple methods to create configured clients
31+
* for testing and development.
32+
*/
33+
public final class TestClient {
34+
35+
private TestClient() {
36+
}
37+
38+
/**
39+
* Creates an A2A client with the specified transport and
40+
* OAuth2 authentication.
41+
*
42+
* @param agentCard the agent card to connect to
43+
* @param messageResponse CompletableFuture for handling responses
44+
* @param transport the transport type to use ("grpc", "rest", or "jsonrpc")
45+
* @return configured A2A client
46+
*/
47+
public static Client createClient(
48+
final AgentCard agentCard,
49+
final CompletableFuture<String> messageResponse,
50+
final String transport) {
51+
52+
// Create consumers for handling client events
53+
List<BiConsumer<ClientEvent, AgentCard>> consumers =
54+
EventHandlerUtil.createEventConsumers(messageResponse);
55+
56+
// Create error handler for streaming errors
57+
Consumer<Throwable> streamingErrorHandler =
58+
EventHandlerUtil.createStreamingErrorHandler(messageResponse);
59+
60+
// Create credential service for OAuth2 authentication
61+
CredentialService credentialService
62+
= new KeycloakOAuth2CredentialService();
63+
64+
// Create shared auth interceptor for all transports
65+
AuthInterceptor authInterceptor = new AuthInterceptor(credentialService);
66+
67+
// Create channel factory for gRPC transport
68+
Function<String, Channel> channelFactory =
69+
agentUrl -> {
70+
return ManagedChannelBuilder
71+
.forTarget(agentUrl)
72+
.usePlaintext()
73+
.build();
74+
};
75+
76+
// Create the A2A client with the specified transport
77+
try {
78+
var builder =
79+
Client.builder(agentCard)
80+
.addConsumers(consumers)
81+
.streamingErrorHandler(streamingErrorHandler);
82+
83+
// Configure only the specified transport
84+
switch (transport.toLowerCase()) {
85+
case "grpc":
86+
builder.withTransport(
87+
GrpcTransport.class,
88+
new GrpcTransportConfigBuilder()
89+
.channelFactory(channelFactory)
90+
.addInterceptor(authInterceptor) // auth config
91+
.build());
92+
break;
93+
case "rest":
94+
builder.withTransport(
95+
RestTransport.class,
96+
new RestTransportConfigBuilder()
97+
.addInterceptor(authInterceptor) // auth config
98+
.build());
99+
break;
100+
case "jsonrpc":
101+
builder.withTransport(
102+
JSONRPCTransport.class,
103+
new JSONRPCTransportConfigBuilder()
104+
.addInterceptor(authInterceptor) // auth config
105+
.build());
106+
break;
107+
default:
108+
throw new IllegalArgumentException(
109+
"Unsupported transport type: "
110+
+ transport
111+
+ ". Supported types are: grpc, rest, jsonrpc");
112+
}
113+
114+
return builder.clientConfig(new ClientConfig.Builder().build()).build();
115+
} catch (Exception e) {
116+
throw new RuntimeException("Failed to create A2A client", e);
117+
}
118+
}
119+
}

0 commit comments

Comments
 (0)