Spring Security Tool Authorization¶
English | 한국어
The optional Spring Security integration limits which Spring AI tools the model
can see and which tools it may execute for the current principal
(Authentication). Tool authorization can be used on its own or together with
privacy protection.
When both are used, Spring Security authorizes tool exposure and execution, while privacy protection detects PII and controls which original values each tool may receive.
Guarantees¶
The integration checks authorization at the following stages of a protected tool-calling path.
| Stage | Guarantee |
|---|---|
| Before model exposure | The authorization policy is evaluated, and denied tools are omitted from the list provided to the model. |
| Model tool request | A tool that was not included in the model's list is not executed, even if the model requests it by name. |
| Multiple tools in one response | Authorization is checked for the complete set of requested tools before execution begins. |
| Immediately before execution | Authorization is checked again for each tool. When privacy protection is also enabled, this happens before any allowed original PII is restored. |
When combining authorization with privacy protection, configure tool permissions and original PII disclosure separately:
AuthorizationManager<ToolAuthorizationContext>decides whether the current principal may discover or execute a tool.tools.disclosuresdecides which PII entity types an authorized tool may receive as original values.
Authorizing a tool does not automatically disclose original PII. An original
value is provided only when the authorization policy allows execution and its
entity type is configured under tools.disclosures.
Add the Spring Boot Starter¶
Gradle¶
dependencies {
implementation "io.github.ultramancode:spring-ai-privacy-guardrails-spring-security-spring-boot-starter:0.3.0"
}
Maven¶
<dependency>
<groupId>io.github.ultramancode</groupId>
<artifactId>spring-ai-privacy-guardrails-spring-security-spring-boot-starter</artifactId>
<version>0.3.0</version>
</dependency>
The Security starter can be used without the base Privacy Guardrails starter.
Tool authorization uses the Authentication established by the application's
existing Spring Security configuration, so it must be available when a request
that uses tools begins.
Tool Authorization Only¶
Register an AuthorizationManager<ToolAuthorizationContext> bean as shown in
Define the Authorization Policy.
The starter then provides a ToolAuthorizationChatClientFactory for
creating clients with tool authorization. No privacy analyzer is required.
With Privacy Protection¶
Add the base Privacy Guardrails starter or an analyzer starter that includes
it, and use the same version for all Privacy Guardrails modules. Configure
Regex, Presidio, OpenNLP, or a custom PiiAnalyzer as described in
Getting Started. When privacy and authorization are both
configured, the starter also provides a PrivacySecurityChatClientFactory.
Define the Authorization Policy¶
Register your tool authorization policy as an
AuthorizationManager<ToolAuthorizationContext> bean. AuthorizationManager
is a standard Spring Security extension interface.
The policy receives user information through Authentication and the tool's
specification (ToolDefinition) and authorization phase through
ToolAuthorizationContext. Tool argument values and the original request text
are not passed to the policy. For example, it can check whether the user may
use customerLookup, but it does not receive the actual customerId being
looked up. Implement checks that depend on argument values, such as permission
to view a particular customer, in the tool or application service.
The following example allows only users with the ROLE_SUPPORT authority to
use customerLookup. All other tools are denied.
@Bean
AuthorizationManager<ToolAuthorizationContext> toolAuthorizationManager() {
return (authentication, context) -> {
Authentication currentAuthentication = authentication.get();
if (currentAuthentication == null || !currentAuthentication.isAuthenticated()) {
return new AuthorizationDecision(false);
}
boolean hasSupportRole = currentAuthentication.getAuthorities().stream()
.anyMatch(authority -> authority.getAuthority().equals("ROLE_SUPPORT"));
boolean granted = switch (context.toolDefinition().name()) {
case "customerLookup" -> hasSupportRole;
default -> false;
};
return new AuthorizationDecision(granted);
};
}
Use context.phase() if you need different rules for each phase. Its value is
ToolAuthorizationPhase.DEFINITION when building the tool list for the model
and ToolAuthorizationPhase.EXECUTION when checking permission to execute a
tool.
If the policy returns null or a denied result, the integration omits the tool
from the list during the definition phase and rejects the call during the
execution phase.
The following example allows all tools:
@Bean
AuthorizationManager<ToolAuthorizationContext> toolAuthorizationManager() {
return (authentication, context) -> new AuthorizationDecision(true);
}
The tool authorization integration captures the request's Authentication
before evaluating the policy. This is required even when the policy allows
all tools.
Configure the ChatClient¶
After adding the dependency and registering the authorization policy bean,
create a ChatClient through a factory as shown below. Choose tool authorization
alone or combine it with privacy protection.
A client that only needs privacy protection can use PrivacyChatClientConfigurer.
In the examples below, customerLookupToolCallback is a customer lookup tool
registered as a bean by the application. Registering it with defaultTools(...)
makes it available to requests through that client.
Apply Tool Authorization Only¶
To use tool authorization on its own, configure the ChatClient as follows:
@Bean
ChatClient authorizedToolClient(
ChatModel chatModel,
ToolAuthorizationChatClientFactory authorizationFactory,
ToolCallback customerLookupToolCallback
) {
return authorizationFactory.builder(chatModel)
.defaultTools(customerLookupToolCallback)
.build();
}
Combine with Privacy Protection¶
When privacy protection is also configured, use
PrivacySecurityChatClientFactory. It applies tool authorization and privacy
protection together:
@Bean
ChatClient securedChatClient(
ChatModel chatModel,
PrivacySecurityChatClientFactory privacySecurityFactory,
PrivacyToolCallbackFactory privacyToolCallbackFactory,
ToolCallback customerLookupToolCallback
) {
ToolCallback protectedCustomerLookup =
privacyToolCallbackFactory.wrap(customerLookupToolCallback);
return privacySecurityFactory.builder(chatModel)
.defaultTools(protectedCustomerLookup)
.build();
}
The combined factory already configures privacy protection. Do not apply
PrivacyChatClientConfigurer again to its builders.
As shown above, wrap tools with PrivacyToolCallbackFactory before registering
them for privacy protection. Configure the PII types to disclose as original
values under tools.disclosures. This example allows only the original customer
ID (CUSTOMER_ID) to reach customerLookup:
Tool Advisor Configuration¶
A tool-calling advisor executes tools requested by the model and passes their
results back to it. ToolCallingAdvisor and the search-enabled
ToolSearchToolCallingAdvisor serve this role and implement Spring AI's
ToolAdvisor interface.
Factory-created clients can also use ordinary advisors, such as conversation
memory or RAG advisors, registered through defaultAdvisors(...) or request-level
advisors(...). When combining them with privacy protection, follow the
advisor ordering guidance so added data
passes through privacy inspection.
The factory configures the tool-calling advisor with authorization checks. For clients that use tools:
- Keep Spring AI's automatic tool-advisor registration enabled.
spring.ai.chat.client.tool-calling.enabledis enabled by default. Do not disable it per request withAdvisorParams.toolCallingAdvisorAutoRegister(false). - Registering a separate tool-calling advisor (a
ToolAdvisorimplementation) withdefaultAdvisors(...)or request-leveladvisors(...)is rejected. To change tool calling, pass its builder to the factory as described below.
Custom Tool Advisor¶
Pass the builder for your chosen tool-calling advisor to
factory.builder(chatModel, toolAdvisorBuilder). The factory connects tool
authorization checks when configuring the client.
The following example sets the tool-calling advisor's execution order to 100
on a client using tool authorization alone:
@Bean
ChatClient customToolAdvisorClient(
ChatModel chatModel,
ToolAuthorizationChatClientFactory authorizationFactory,
ToolCallback customerLookupToolCallback
) {
ToolCallingAdvisor.Builder<?> toolAdvisorBuilder =
ToolCallingAdvisor.builder().advisorOrder(100);
return authorizationFactory.builder(chatModel, toolAdvisorBuilder)
.defaultTools(customerLookupToolCallback)
.build();
}
100 is an example value; choose an execution order that fits the other advisors
in your application. Orders incompatible with the protection setup are rejected.
The factory supports ToolCallingAdvisor.Builder<?> and its subclasses.
See the Tool Search example for passing a tool-search advisor builder.
Requirements when implementing an advisor or builder
The factory copies the supplied builder, sets an authorization-aware
ToolCallingManager on the copy, and builds the advisor. Custom advisors and
builders must support this process:
- A builder returned by
copy()must still create the same type of advisor and retain the existing settings. - The advisor produced by
build()must use theToolCallingManagerset by the factory throughtoolCallingManager(...)so tool authorization checks apply. - Do not implement
PriorityOrderedon a tool-calling advisor. It could start tool calling before authorization checks, so the factory rejects it. Set the execution order through the builder'sadvisorOrder(...)instead.
The following minimal example extends ToolCallingAdvisor. It uses the default
tool-calling behavior and implements builder copying and advisor creation.
final class CustomToolCallingAdvisor extends ToolCallingAdvisor {
private CustomToolCallingAdvisor(ToolCallingManager manager,
ToolExecutionEligibilityChecker checker, int order, boolean history) {
super(manager, checker, order, history);
}
static final class Builder extends ToolCallingAdvisor.Builder<Builder> {
@Override
public Builder copy() {
return (Builder) super.copy();
}
@Override
protected Builder newCopy() {
return new Builder();
}
@Override
public CustomToolCallingAdvisor build() {
ToolCallingManager manager = getToolCallingManager();
return new CustomToolCallingAdvisor(
manager,
getToolExecutionEligibilityChecker(),
getAdvisorOrder(),
isConversationHistoryEnabled()
);
}
}
}
super.copy() calls copy() in the parent class, ToolCallingAdvisor.Builder.
The three methods have the following roles:
| Method | Role in this example |
|---|---|
copy() |
Delegates copying to the parent's copy() and returns the result as this example's Builder type. |
newCopy() |
Called by the parent's copy(). Creates the new Builder that will receive the copied settings. |
build() |
Reads the ToolCallingManager set by the factory through getToolCallingManager() and passes it, along with the remaining settings, to the CustomToolCallingAdvisor constructor. |
The factory sets the authorization-aware ToolCallingManager on the builder.
Your build() implementation reads it through getToolCallingManager() and
passes it to the advisor constructor. The manager variable above holds this value.
The constructor passes that same ToolCallingManager to the parent
ToolCallingAdvisor as the first argument of super(manager, checker, order, history).
The remaining three arguments configure tool execution eligibility, advisor
execution order, and conversation history. The parent advisor uses the supplied
ToolCallingManager to call tools.
The parent's copy() copies the ToolCallingManager, execution eligibility checker,
execution order, and conversation history setting into the new builder.
If your builder adds configuration fields, assign their values to the copy
returned by super.copy() inside your copy() implementation, then return that
copy. As shown above, newCopy() is responsible for creating the new builder.
Pass this builder to the factory when creating the client:
ChatClient client = authorizationFactory
.builder(chatModel, new CustomToolCallingAdvisor.Builder().advisorOrder(100))
.defaultTools(customerLookupToolCallback)
.build();
ToolCallingManager Selection¶
ToolCallingManager handles the actual execution of tools. When a tool policy
bean is present, the starter's default setup uses Spring AI's
DefaultToolCallingManager bean.
The default setup fails at startup if that bean is missing or multiple candidates
are present. To select a ToolCallingManager explicitly, provide a
SpringSecurityToolBoundary bean as shown below.
Custom ToolCallingManager¶
A ToolCallback is an object that invokes a tool. When delegating execution, the
library wraps each application tool callback with a permission recheck and places
the wrapped callbacks in the tool options of the Prompt passed to your custom
ToolCallingManager. Each wrapper checks permission before invoking the callback
it wraps.
When combining authorization with privacy protection, the library automatically
adds this permission-checking wrapper around the callback wrapped by
PrivacyToolCallbackFactory. Your custom ToolCallingManager must invoke the
outermost callback supplied in the Prompt.
This example delegates execution to Spring AI's default implementation,
DefaultToolCallingManager. Passing the received Prompt and model response
unchanged lets this implementation use the callbacks in the Prompt, preserving
the permission recheck.
@Bean
SpringSecurityToolBoundary springSecurityToolBoundary(
ToolCallingManager delegate,
AuthorizationManager<ToolAuthorizationContext> authorizationManager
) {
ToolCallingManager customManager = new ToolCallingManager() {
@Override
public List<ToolDefinition> resolveToolDefinitions(ToolCallingChatOptions options) {
return delegate.resolveToolDefinitions(options);
}
@Override
public ToolExecutionResult executeToolCalls(Prompt prompt, ChatResponse response) {
return delegate.executeToolCalls(prompt, response);
}
};
return SpringSecurityToolBoundary.builder(customManager, authorizationManager)
.build();
}
The example receives Spring AI's DefaultToolCallingManager bean through the
delegate parameter, declared as ToolCallingManager. If you already have a
custom ToolCallingManager, pass that object in place of customManager.
In this example, delegate.executeToolCalls(prompt, response) finds the callbacks
and passes along ToolContext, which carries additional data needed for tool
execution. The delegating code does not need to implement callback selection or
context passing separately.
Any custom ToolCallingManager you connect must execute tools through the callbacks
in the supplied Prompt. These callbacks recheck permission immediately before
tool execution, so calling the original tool separately skips this check. Pass the
tool context as well to preserve data needed for execution, including privacy
protection when enabled.
The starter's factories use the SpringSecurityToolBoundary bean shown above
when configuring clients. Create a client as shown in
Configure the ChatClient and call tools through that
client.
Without the Spring Boot Starter¶
When using spring-ai-privacy-guardrails-spring-security directly, create a
SpringSecurityToolBoundary and pass it to ToolAuthorizationChatClientFactory.
The factory configures the client to capture the request's authentication and
check permissions before exposing tools to the model and before executing tool calls.
The following example creates a client with tool authorization only, using your model, authorization policy, and customer lookup tool.
ChatClient createAuthorizedClient(
ChatModel chatModel,
AuthorizationManager<ToolAuthorizationContext> authorizationManager,
ToolCallback customerLookupToolCallback
) {
SpringSecurityToolBoundary boundary = SpringSecurityToolBoundary.builder(
ToolCallingManager.builder().build(), authorizationManager
).build();
ToolAuthorizationChatClientFactory authorizationFactory =
new ToolAuthorizationChatClientFactory(boundary);
return authorizationFactory.builder(chatModel)
.defaultTools(customerLookupToolCallback)
.build();
}
The factory places the tool-calling advisor before the common model request boundary. Do not register a separate tool advisor; pass its builder to the factory when customizing the tool loop.
Callback additions and replacements performed by application advisors must
finish before boundary.toolAuthorizationAdvisor() captures the request's tool
list.
To include privacy protection without Spring Boot, create a
PrivacyChatClientConfigurer from the privacy integration module and pass it
with this authorization factory to PrivacySecurityChatClientFactory.
Both factories support builderWithBoundary(model, additionalConfigurer) and
builder(model, toolAdvisorBuilder, additionalConfigurer) for composing extra
features, such as final model-request inspection, into the same boundary.
Do not apply another boundary configurer separately to the returned builder.
Tool Search¶
Tool Search is an optional Spring AI feature that lets a model search for the tools it needs.
Add org.springframework.ai:spring-ai-tool-search-advisor, using
the version managed by your application's Spring AI BOM.
The following example combines tool authorization, Tool Search, and privacy
protection by passing the tool-search advisor's builder to the factory.
ToolIndex is the tool search index provided as a bean by the application.
@Bean
ChatClient toolSearchClient(
ChatModel chatModel,
PrivacySecurityChatClientFactory privacySecurityFactory,
ToolIndex toolIndex
) {
return privacySecurityFactory.builder(chatModel,
ToolSearchToolCallingAdvisor.builder().toolIndex(toolIndex))
.build();
}
For tool authorization alone, replace the injected parameter
PrivacySecurityChatClientFactory privacySecurityFactory in the code directly
above with ToolAuthorizationChatClientFactory authorizationFactory.
Update the return statement to call authorizationFactory.builder(...).
Pass the same model and tool-search advisor builder to builder(...).
Register tools with defaultTools(...) when building the client or with
tools(...) on each request. When also applying privacy protection, register
callbacks wrapped by PrivacyToolCallbackFactory.wrap(...).
Tool Search also requires a conversation ID on each request. The following example
registers a tool and passes the application's conversationId to a client that
also uses privacy protection:
ToolCallback protectedCustomerLookup =
privacyToolCallbackFactory.wrap(customerLookupToolCallback);
String response = toolSearchClient.prompt()
.user("Find customer CUST-123456.")
.tools(protectedCustomerLookup)
.advisors(spec -> spec.param(ChatMemory.CONVERSATION_ID, conversationId))
.call()
.content();
With this setup, only authorized tools registered for the request are searchable, and permission is checked again immediately before a selected tool executes. When privacy protection is also used, detected PII in the search query is replaced with opaque tokens before the search runs. These replacement strings do not directly reveal the original values.
The library allows the tool that performs the search; the application's authorization policy applies to the tools being searched. Tool replacements and unsupported additions during a request are rejected before execution.
If you configure Tool Search directly with a ToolCallingManager that does not
apply authorization, denied tools may also become searchable. See the
Threat Model for considerations when configuring this yourself.
Blocking, Reactive, and Asynchronous Context¶
When a tool runs on a different thread during a request, its authorization
policy still receives the user's Authentication obtained at the start of
that request.
- Blocking calls: Authentication is obtained from the Spring Security context when the request begins.
- Streaming: The Reactor security context takes precedence. The calling thread's authentication is used only when no Reactor security context is present. A Reactor security context that is registered but empty causes the request to be rejected.
- Asynchronous calls: If the
ChatClientinvocation itself starts on another thread, propagate Spring Security authentication to it. UseDelegatingSecurityContextExecutorServicefor the executor running the task to propagate authentication. The same requirement applies to virtual threads.
A request with registered tools is rejected if it has no Authentication.
The captured Authentication represents the request identity. Authorization is
still evaluated again at execution time, but detecting a permission revocation
during a long-running request requires the application policy to consult current
external state rather than relying only on authorities captured in the object.
The library cleans up request authorization state when a blocking call completes or fails, or when a stream completes, fails, or is cancelled.
Compatibility¶
| Component | Supported versions |
|---|---|
| Java | 17 or later |
| Spring AI | 2.0.x |
| Spring Boot | 4.x |
| Spring Security | 7.x |