|
| 1 | +--- |
| 2 | +layout: post |
| 3 | +title: 'How to Secure Your A2A Server Agent with Keycloak OAuth2' |
| 4 | +date: 2025-10-28 |
| 5 | +tags: ai a2a security keycloak oauth2 |
| 6 | +synopsis: This blog post shows how to secure an A2A server agent using OAuth2 bearer tokens via Keycloak and shows how to enable an A2A client to automatically obtain and pass the required token in each request. |
| 7 | +author: fjuma |
| 8 | +--- |
| 9 | + |
| 10 | +Today, we've released A2A Java SDK 0.3.0.Final which includes security and cloud related enhancements. |
| 11 | +In this post, we'll focus on A2A security. Stay tuned for a future post on cloud related enhancements! |
| 12 | + |
| 13 | +The A2A protocol delegates authentication to standard mechanisms like OAuth2 and OpenID Connect. An |
| 14 | +A2A server agent specifies its authentication requirements in its agent card so A2A clients know |
| 15 | +what type of credentials they need to obtain and then they can pass these credentials to |
| 16 | +the server agent when sending requests. |
| 17 | + |
| 18 | +We're going to walk through securing an A2A server agent with OAuth2 using Keycloak and we'll |
| 19 | +show how to configure an A2A client to handle token management. Our A2A server agent will be able |
| 20 | +to support all 3 transports (JSON‑RPC, HTTP+JSON/REST, and gRPC) so we can see that the authentication |
| 21 | +configuration is consistent across transport protocols. |
| 22 | + |
| 23 | +== Magic 8 Ball Sample |
| 24 | + |
| 25 | +To see security configuration in action, we'll use the https://github.com/a2aproject/a2a-samples/tree/main/samples/java/agents/magic_8_ball_security[Magic 8 Ball Security sample] |
| 26 | +from the https://github.com/a2aproject/a2a-samples[a2a-samples] repository. This sample is a simple https://github.com/a2aproject/a2a-samples/blob/main/samples/java/agents/magic_8_ball_security/server/src/main/java/com/samples/a2a/Magic8BallAgent.java[Quarkus LangChain4j AI service] that consults a virtual Magic 8 Ball to answer yes/no questions. |
| 27 | + |
| 28 | +=== A2A Server Agent |
| 29 | + |
| 30 | +We're going to focus specifically on the security configuration for our A2A server agent. For a detailed |
| 31 | +description on how to turn a Quarkus LangChain4j AI Service into an A2A server agent, check |
| 32 | +out this previous https://quarkus.io/blog/quarkus-a2a-java-grpc/[blog post]. |
| 33 | + |
| 34 | +==== Security Configuration |
| 35 | + |
| 36 | +When using the A2A Java SDK reference implementations, the endpoints that need to be secured according to |
| 37 | +the A2A specification are already annotated with `@Authenticated` for you. That means that in order to secure |
| 38 | +your A2A server agent, you just need to specify the configuration for the specific authentication mechanism |
| 39 | +you'd like to use. To secure our A2A server agent with OAuth2, we simply need to add a dependency on the |
| 40 | +`quarkus-oidc` extension as shown below: |
| 41 | + |
| 42 | +[source,java] |
| 43 | +---- |
| 44 | +<dependency> |
| 45 | + <groupId>io.quarkus</groupId> |
| 46 | + <artifactId>quarkus-oidc</artifactId> |
| 47 | +</dependency> |
| 48 | +---- |
| 49 | + |
| 50 | +> **NOTE**: In our sample, we're going to rely on Quarkus Dev Services to automatically create and configure |
| 51 | +a Keycloak instance that we'll use as our OAuth2 provider. Quarkus Dev Services relies on a container runtime |
| 52 | +like Docker or Podman to be installed and properly configured. For more details on using Podman with |
| 53 | +Quarkus, see this https://quarkus.io/guides/podman[guide]. |
| 54 | + |
| 55 | +Now that our A2A server agent is being secured with OAuth2, we need to indicate this in our agent card |
| 56 | +as shown below. |
| 57 | + |
| 58 | +[source,java] |
| 59 | +---- |
| 60 | +@Produces |
| 61 | + @PublicAgentCard |
| 62 | + public AgentCard agentCard() { |
| 63 | + ClientCredentialsOAuthFlow clientCredentialsOAuthFlow = new ClientCredentialsOAuthFlow( <1> |
| 64 | + null, <2> |
| 65 | + Map.of("openid", "openid", "profile", "profile"), <3> |
| 66 | + "http://localhost:" + keycloakPort + "/realms/quarkus/protocol/openid-connect/token"); <4> |
| 67 | + OAuth2SecurityScheme securityScheme = new OAuth2SecurityScheme.Builder() <5> |
| 68 | + .flows(new OAuthFlows.Builder().clientCredentials(clientCredentialsOAuthFlow).build()) |
| 69 | + .build(); |
| 70 | +
|
| 71 | + return new AgentCard.Builder() |
| 72 | + .name("Magic 8 Ball Agent") |
| 73 | + .description( |
| 74 | + "A mystical fortune-telling agent that answers your yes/no " |
| 75 | + + "questions by asking the all-knowing Magic 8 Ball oracle.") |
| 76 | + .preferredTransport(TransportProtocol.JSONRPC.asString()) |
| 77 | + .url("http://localhost:" + httpPort) |
| 78 | + .version("1.0.0") |
| 79 | + .documentationUrl("http://example.com/docs") |
| 80 | + .capabilities( |
| 81 | + new AgentCapabilities.Builder() |
| 82 | + .streaming(true) |
| 83 | + .pushNotifications(false) |
| 84 | + .stateTransitionHistory(false) |
| 85 | + .build()) |
| 86 | + .defaultInputModes(List.of("text")) |
| 87 | + .defaultOutputModes(List.of("text")) |
| 88 | + .security(List.of(Map.of(OAuth2SecurityScheme.OAUTH2, <6> |
| 89 | + List.of("profile")))) |
| 90 | + .securitySchemes(Map.of(OAuth2SecurityScheme.OAUTH2, securityScheme)) <7> |
| 91 | + .skills( |
| 92 | + List.of( |
| 93 | + new AgentSkill.Builder() |
| 94 | + .id("magic_8_ball") |
| 95 | + .name("Magic 8 Ball Fortune Teller") |
| 96 | + .description("Uses a Magic 8 Ball to answer" |
| 97 | + + " yes/no questions") |
| 98 | + .tags(List.of("fortune", "magic-8-ball", "oracle")) |
| 99 | + .examples( |
| 100 | + List.of( |
| 101 | + "Should I deploy this code on Friday?", |
| 102 | + "Will my tests pass?", |
| 103 | + "Is this a good idea?")) |
| 104 | + .build())) |
| 105 | + .protocolVersion("0.3.0") |
| 106 | + .additionalInterfaces( <8> |
| 107 | + List.of( |
| 108 | + new AgentInterface( |
| 109 | + TransportProtocol.JSONRPC.asString(), |
| 110 | + "http://localhost:" + httpPort), |
| 111 | + new AgentInterface( |
| 112 | + TransportProtocol.HTTP_JSON.asString(), |
| 113 | + "http://localhost:" + httpPort), |
| 114 | + new AgentInterface(TransportProtocol.GRPC.asString(), |
| 115 | + "localhost:" + httpPort))) |
| 116 | + .build(); |
| 117 | + } |
| 118 | +---- |
| 119 | +<1> Details about the OAuth2 flow our A2A server agent supports. |
| 120 | +<2> We can optionally specify the URL to be used for obtaining a refresh token. |
| 121 | +<3> The available scopes for the OAuth2 client credentials flow. This is a map between the scope name and a description of the scope. |
| 122 | +<4> The token URL to be used for this flow. |
| 123 | +<5> Specifies an OAuth2 security scheme using the `ClientCredentialsOAuthFlow`. We'll refer to this from the |
| 124 | +agent card. |
| 125 | +<6> A list of security requirement objects that apply to all agent interactions. Each object lists |
| 126 | +security schemes that can be used. Follows the OpenAPI 3.0 Security Requirement Object. |
| 127 | +<7> A declaration of the security schemes that can be used. |
| 128 | +<8> Notice that our A2A server agent supports all 3 transports: JSON-RPC, HTTP+JSON/REST, and gRPC. |
| 129 | + |
| 130 | +==== Starting the A2A Server Agent |
| 131 | + |
| 132 | +Follow the https://github.com/a2aproject/a2a-samples/tree/main/samples/java/agents/magic_8_ball_security#running-the-a2a-server-agent[instructions] in the sample's README to start our A2A server agent. |
| 133 | + |
| 134 | +A2A clients can now send queries to our A2A server agent using any of the 3 configured transports. |
| 135 | + |
| 136 | +Now that our secured A2A server agent is up and running, let's take a look at how to create an A2A |
| 137 | +client that can communicate with it. |
| 138 | + |
| 139 | +=== A2A Client |
| 140 | + |
| 141 | +The `magic_8_ball_security` sample also includes a https://github.com/a2aproject/a2a-samples/tree/main/samples/java/agents/magic_8_ball_security/client/src/main/java/com/samples/a2a/client/TestClient.java[TestClient] that can be used to send messages to the `Magic8BallAgent`. |
| 142 | + |
| 143 | +For general information on how to configure A2A clients using the A2A Java SDK, check out this https://quarkus.io/blog/quarkus-a2a-java-0-3-0-alpha-release/[previous post]. |
| 144 | + |
| 145 | +=== Security Configuration |
| 146 | + |
| 147 | +Because the A2A server agent our client will be communicating with is secured using OAuth2, our client |
| 148 | +needs to be able to obtain the required token and pass it to the A2A server agent with each request. |
| 149 | + |
| 150 | +The `a2a-java-sdk-client` dependency provided by the A2A Java SDK gives us access to a `Client.builder` that we'll use to create our A2A client and specify the necessary authentication configuration. |
| 151 | + |
| 152 | +The A2A Java SDK provides two main classes related to authentication: |
| 153 | + |
| 154 | +* `CredentialService`: An interface you can implement to define how to obtain a credential for a specific security scheme. |
| 155 | + |
| 156 | +* `AuthInterceptor`: A `ClientCallInterceptor` implementation that uses a `CredentialService` to automatically obtain and attach the required credential to client requests. |
| 157 | + |
| 158 | +Let's see how to configure an A2A client using these classes. |
| 159 | + |
| 160 | +[source,java] |
| 161 | +---- |
| 162 | +// Create credential service for OAuth2 authentication |
| 163 | +CredentialService credentialService = new KeycloakOAuth2CredentialService(); <1> |
| 164 | +
|
| 165 | +// Create an auth interceptor to be used for all transports |
| 166 | +AuthInterceptor authInterceptor = new AuthInterceptor(credentialService); <2> |
| 167 | +
|
| 168 | +... |
| 169 | +
|
| 170 | +var builder = Client.builder(agentCard) |
| 171 | + .addConsumers(consumers) |
| 172 | + .streamingErrorHandler(streamingErrorHandler); |
| 173 | +
|
| 174 | +// Our client will optionally allow the user to specify which transport to use. |
| 175 | +// Here, we'll add configuration for the user-specified transport. The transport |
| 176 | +// will default to jsonrpc if not specified by the user. |
| 177 | +switch (transport.toLowerCase()) { |
| 178 | + case "grpc": |
| 179 | + builder.withTransport( |
| 180 | + GrpcTransport.class, |
| 181 | + new GrpcTransportConfigBuilder() |
| 182 | + .channelFactory(channelFactory) |
| 183 | + .addInterceptor(authInterceptor) <3> |
| 184 | + .build()); |
| 185 | + break; |
| 186 | + case "rest": |
| 187 | + builder.withTransport( |
| 188 | + RestTransport.class, |
| 189 | + new RestTransportConfigBuilder() |
| 190 | + .addInterceptor(authInterceptor) <4> |
| 191 | + .build()); |
| 192 | + break; |
| 193 | + case "jsonrpc": |
| 194 | + builder.withTransport( |
| 195 | + JSONRPCTransport.class, |
| 196 | + new JSONRPCTransportConfigBuilder() |
| 197 | + .addInterceptor(authInterceptor) <5> |
| 198 | + .build()); |
| 199 | + break; |
| 200 | + default: |
| 201 | + throw new IllegalArgumentException("Unsupported transport type. Supported types are: grpc, rest, jsonrpc"); |
| 202 | +} |
| 203 | +
|
| 204 | +return builder.build(); |
| 205 | +---- |
| 206 | +<1> `CredentialService` is an interface provided by the A2A Java SDK. You can implement this interface to |
| 207 | +obtain credentials for a given security scheme. In our sample, since the A2A server agent is being secured |
| 208 | +with Keycloak, we have created a class called https://github.com/a2aproject/a2a-samples/tree/main/samples/java/agents/magic_8_ball_security/client/src/main/java/com/samples/a2a/client/KeycloakOAuth2CredentialService.java[KeycloakOAuth2CredentialService] that implements this |
| 209 | +interface and obtains credentials for the OAuth2 security scheme using the Keycloak `AuthzClient`. |
| 210 | +<2> `AuthInterceptor` is a class provided by the A2A Java SDK that can be used to automatically add credential |
| 211 | +details to a request based on the security schemes supported by the A2A server agent using a `CredentialService`. |
| 212 | +Notice that we are passing our `KeycloakOAuth2CredentialService` to the `AuthInterceptor`. We're going to use |
| 213 | +the same `AuthInterceptor` to specify the authentication configuration for all 3 transport protocols. |
| 214 | +<3> Interceptors can be configured for each transport. Here we are specifying that we want to use our `AuthInterceptor` for the gRPC transport. |
| 215 | +<4> This shows how to configure the `AuthInterceptor` for the HTTP+JSON/REST transport. |
| 216 | +<5> This shows how to configure the `AuthInterceptor` for the JSON-RPC transport. |
| 217 | + |
| 218 | +With this configuration, when our A2A client attempts to sends a request to the A2A server agent, |
| 219 | +the `AuthInterceptor` will use the A2A server's agent card to detect its supported security schemes |
| 220 | +and will automatically obtain the required credential for the OAuth2 security scheme using the `KeycloakOAuth2CredentialService`. The obtained token will then be included in the HTTP authorization |
| 221 | +header for the A2A server agent to validate. |
| 222 | + |
| 223 | +=== Using the A2A Client |
| 224 | + |
| 225 | +The sample application contains a `TestClientRunner` that can be run using JBang: |
| 226 | + |
| 227 | +[source,shell] |
| 228 | +---- |
| 229 | +jbang TestClientRunner.java |
| 230 | +---- |
| 231 | + |
| 232 | +You should see output similar to this: |
| 233 | + |
| 234 | +[source,shell] |
| 235 | +---- |
| 236 | +Connecting to agent at: http://localhost:11000 |
| 237 | +Using transport: jsonrpc |
| 238 | +... |
| 239 | +Sending message: Should I deploy this code on Friday? |
| 240 | +Using jsonrpc transport with OAuth2 Bearer token |
| 241 | +Message sent successfully. Waiting for response... |
| 242 | +Received status-update: submitted |
| 243 | +Received status-update: working |
| 244 | +Received artifact-update: The Magic 8 Ball says: "Outlook good." It seems like a Friday deployment might be a good idea! What are your thoughts on that? |
| 245 | +Received status-update: completed |
| 246 | +Final response: The Magic 8 Ball says: "Outlook good." It seems like a Friday deployment might be a good idea! What are your thoughts on that? |
| 247 | +---- |
| 248 | + |
| 249 | +You can also experiment with sending different messages to the A2A server agent using the --message option as |
| 250 | +follows: |
| 251 | + |
| 252 | +[source,shell] |
| 253 | +---- |
| 254 | +jbang TestClientRunner.java --message "Should I refactor this code?" |
| 255 | +---- |
| 256 | + |
| 257 | +You can try using different transports (`jsonrpc`, `grpc`, or `rest`) with the --transport option: |
| 258 | + |
| 259 | +[source,shell] |
| 260 | +---- |
| 261 | +jbang TestClientRunner.java --transport grpc |
| 262 | +---- |
| 263 | + |
| 264 | +== Conclusion |
| 265 | + |
| 266 | +This post has shown how to configure security for both A2A server agents and A2A clients. |
| 267 | + |
| 268 | +=== Further Reading |
| 269 | + |
| 270 | +* https://github.com/a2aproject/a2a-samples/tree/main/samples/java/agents/magic_8_ball_security[Magic 8 Ball Security Sample] |
| 271 | +* https://quarkus.io/blog/quarkus-a2a-java-0-3-0-alpha-release/[Getting Started with Quarkus and A2A Java SDK 0.3.0] |
| 272 | +* https://quarkus.io/blog/quarkus-a2a-java-0-3-0-beta-release/[A2A Java SDK: Support for the REST Transport is Now Here] |
| 273 | +* https://quarkus.io/blog/quarkus-a2a-java-grpc/[Getting Started with A2A Java SDK and gRPC] |
| 274 | +* https://github.com/a2aproject/a2a-samples/tree/main/samples/java/agents[A2A Java SDK Samples] |
| 275 | +* https://github.com/a2aproject/a2a-java/blob/main/README.md[A2A Java SDK Documentation] |
| 276 | +* https://a2a-protocol.org/latest/specification/[A2A Specification] |
0 commit comments