Overview
HandsAI supports multiple authentication methods to connect with virtually any REST API. Authentication is configured at the Provider level and automatically applied to all tools under that provider.
All credentials are encrypted at rest using Jasypt before being stored in SQLite.
Supported Authentication Types
HandsAI supports these authentication methods:
No authentication required (public APIs)
Static API key sent in header, query parameter, or request body
Bearer token authentication (typically sent in Authorization header)
HTTP Basic Authentication (username:password encoded in Base64)
AuthenticationTypeEnum
public enum AuthenticationTypeEnum {
NONE ,
API_KEY ,
BEARER_TOKEN ,
BASIC_AUTH
}
API Key Locations
When using API_KEY or BEARER_TOKEN authentication, you can specify where the credential should be sent:
Send in HTTP header (most common) Example: Authorization: Bearer sk_abc123
Append to URL as query parameter Example: ?api_key=sk_abc123
Include in request body (for POST/PUT/PATCH requests) Example: {"api_key": "sk_abc123", "query": "..."}
ApiKeyLocationEnum
public enum ApiKeyLocationEnum {
NONE ,
HEADER ,
QUERY_PARAMETER ,
IN_BODY
}
Authentication Configuration Examples
{
"name" : "GitHub REST API" ,
"baseUrl" : "https://api.github.com" ,
"authenticationType" : "BEARER_TOKEN" ,
"apiKeyLocation" : "HEADER" ,
"apiKeyName" : "Authorization" ,
"apiKeyValue" : "ghp_abc123def456" ,
"customHeaders" : {
"Accept" : "application/vnd.github+json" ,
"X-GitHub-Api-Version" : "2022-11-28" ,
"User-Agent" : "HandsAI/1.0"
}
}
Requests to GitHub include:
Authorization: Bearer ghp_abc123def456
Accept: application/vnd.github+json
X-GitHub-Api-Version: 2022-11-28
User-Agent: HandsAI/1.0
Example 2: Tavily (API Key in Body)
{
"name" : "Tavily AI Search" ,
"baseUrl" : "https://api.tavily.com" ,
"authenticationType" : "API_KEY" ,
"apiKeyLocation" : "IN_BODY" ,
"apiKeyName" : "api_key" ,
"apiKeyValue" : "tvly-abc123" ,
"customHeaders" : {
"Content-Type" : "application/json"
}
}
Requests to Tavily automatically inject the API key in the body:
{
"api_key" : "tvly-abc123" ,
"query" : "latest AI news" ,
"search_depth" : "basic"
}
{
"name" : "Resend API" ,
"baseUrl" : "https://api.resend.com" ,
"authenticationType" : "BEARER_TOKEN" ,
"apiKeyLocation" : "HEADER" ,
"apiKeyName" : "Authorization" ,
"apiKeyValue" : "re_abc123def456" ,
"customHeaders" : {
"Content-Type" : "application/json" ,
"User-Agent" : "HandsAI/1.0"
}
}
Requests to Resend include:
Authorization: Bearer re_abc123def456
Content-Type: application/json
Example 4: Custom API (API Key in Query Parameter)
{
"name" : "Custom Weather API" ,
"baseUrl" : "https://api.weather.example.com" ,
"authenticationType" : "API_KEY" ,
"apiKeyLocation" : "QUERY_PARAMETER" ,
"apiKeyName" : "apikey" ,
"apiKeyValue" : "abc123def456"
}
Requests append the key to the URL:
GET https://api.weather.example.com/forecast?city=London&apikey=abc123def456
OAuth2 Token Refresh (Dynamic Authentication)
Some APIs require fetching a short-lived access token before making requests. HandsAI supports Dynamic Authentication for this scenario.
How Dynamic Auth Works
Configure Token Endpoint : Specify the OAuth2 token URL and credentials
First Request : HandsAI fetches the access token
Token Caching : Token is cached for 5 minutes (default TTL)
Automatic Refresh : When the token expires, HandsAI fetches a new one
Tool Execution : The fresh token is used for API calls
DynamicTokenManager
HandsAI uses DynamicTokenManager to handle OAuth2 token lifecycle:
@ Service
public class DynamicTokenManager {
private final Map < Long , CachedToken > tokenCache = new ConcurrentHashMap <>();
private static final long TOKEN_TTL_SECONDS = 300 ; // 5 minutes
public String getToken ( ApiProvider provider ) {
if ( ! provider . isDynamicAuth ()) {
return null ; // Not using dynamic auth
}
CachedToken cachedToken = tokenCache . get ( provider . getId ());
if (cachedToken != null && cachedToken . expiresAt (). isAfter ( Instant . now ())) {
log . debug ( "Returning cached dynamic token for provider {}" , provider . getId ());
return cachedToken . token ();
}
log . info ( "Fetching new dynamic token for provider {}" , provider . getId ());
String newToken = fetchNewToken (provider);
tokenCache . put ( provider . getId (),
new CachedToken (newToken, Instant . now (). plusSeconds (TOKEN_TTL_SECONDS)));
return newToken;
}
public void invalidateToken ( Long providerId ) {
log . info ( "Invalidating dynamic token cache for provider {}" , providerId);
tokenCache . remove (providerId);
}
private record CachedToken ( String token, Instant expiresAt) {}
}
Token Fetch Flow
When fetching a new token, DynamicTokenManager performs these steps:
private String fetchNewToken ( ApiProvider provider) {
try {
RestClient client = restClientBuilder . baseUrl ( provider . getDynamicAuthUrl ()). build ();
HttpMethod method = provider . getDynamicAuthMethod () == DynamicAuthMethodEnum . GET
? HttpMethod . GET
: HttpMethod . POST ;
// Parse and decrypt payload
Map < String , Object > payloadMap = null ;
if ( provider . getDynamicAuthPayload () != null && ! provider . getDynamicAuthPayload (). isBlank ()) {
payloadMap = objectMapper . readValue ( provider . getDynamicAuthPayload (),
new TypeReference < Map < String , Object >>() {});
// Decrypt encrypted values
for ( Map . Entry < String , Object > entry : payloadMap . entrySet ()) {
if ( entry . getValue () instanceof String strVal && ! strVal . isBlank ()) {
entry . setValue ( encryptionService . decrypt (strVal));
}
}
}
String finalUri = provider . getDynamicAuthUrl ();
DynamicAuthPayloadLocationEnum location = provider . getDynamicAuthPayloadLocation ();
// Handle payload location (QUERY_PARAMETERS, HEADERS, BODY)
RestClient . RequestBodySpec requestSpec = buildAuthRequest (client, method, finalUri, payloadMap, location, provider);
String responseBody = requestSpec . retrieve (). body ( String . class );
// Extract token from response using JSON path
return extractTokenFromResponse (responseBody, provider . getDynamicAuthTokenExtractionPath ());
} catch ( Exception e ) {
log . error ( "Failed to fetch dynamic token for provider {}" , provider . getId (), e);
throw new ToolExecutionException ( "Failed to fetch dynamic auth token: " + e . getMessage ());
}
}
Dynamic Auth Configuration Fields
Enable dynamic token fetching
OAuth2 token endpoint URL (e.g., https://bsky.social/xrpc/com.atproto.server.createSession)
HTTP method to fetch token: GET or POST
JSON payload with credentials (encrypted before storage) Example: {"identifier": "[email protected] ", "password": "app_password"}
Payload format: JSON or FORM_DATA
dynamicAuthPayloadLocation
Where to send payload: BODY, HEADERS, or QUERY_PARAMETERS
JSON path to extract token from response (e.g., accessJwt or data.token)
dynamicAuthInvalidationKeywords
Comma-separated keywords that indicate token expiration (e.g., ExpiredToken,AuthenticationRequired)
Example: Bluesky OAuth2 Flow
{
"name" : "Bluesky API" ,
"baseUrl" : "https://bsky.social/xrpc" ,
"authenticationType" : "BEARER_TOKEN" ,
"apiKeyLocation" : "HEADER" ,
"apiKeyName" : "Authorization" ,
"isDynamicAuth" : true ,
"dynamicAuthUrl" : "https://bsky.social/xrpc/com.atproto.server.createSession" ,
"dynamicAuthMethod" : "POST" ,
"dynamicAuthPayloadType" : "JSON" ,
"dynamicAuthPayloadLocation" : "BODY" ,
"dynamicAuthPayload" : "{ \" identifier \" : \" your_handle.bsky.social \" , \" password \" : \" your_app_password \" }" ,
"dynamicAuthTokenExtractionPath" : "accessJwt" ,
"dynamicAuthInvalidationKeywords" : "ExpiredToken,AuthenticationRequired"
}
Token Fetch Request:
POST https://bsky.social/xrpc/com.atproto.server.createSession
Content-Type: application/json
{
"identifier" : "your_handle.bsky.social",
"password" : "your_app_password"
}
Token Response:
{
"did" : "did:plc:abc123" ,
"handle" : "your_handle.bsky.social" ,
"email" : "[email protected] " ,
"accessJwt" : "eyJhbGciOiJIUzI1NiIs..." ,
"refreshJwt" : "eyJhbGciOiJIUzI1NiIs..."
}
HandsAI extracts accessJwt and uses it for subsequent requests:
GET https://bsky.social/xrpc/app.bsky.feed.getTimeline?limit= 10
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...
Token Cache and Expiration
Tokens are cached in-memory with a 5-minute TTL:
private record CachedToken ( String token, Instant expiresAt) {}
Behavior:
Cache Hit : If token exists and expiresAt > now(), use cached token
Cache Miss : Fetch new token, cache it for 5 minutes
Manual Invalidation : Call invalidateToken(providerId) to force refresh
This prevents excessive token requests while ensuring fresh credentials.
Credential Encryption (EncryptionService)
All sensitive credentials are encrypted before storage using Jasypt (Java Simplified Encryption).
EncryptionService Interface
public interface EncryptionService {
String encrypt ( String data );
String decrypt ( String encryptedData );
}
Implementation with Jasypt
@ Service
@ RequiredArgsConstructor
public class EncryptionServiceImpl implements EncryptionService {
private final StringEncryptor stringEncryptor ;
@ Override
public String encrypt ( String data ) {
if (data == null ) {
return null ;
}
return stringEncryptor . encrypt (data);
}
@ Override
public String decrypt ( String encryptedData ) {
if (encryptedData == null ) {
return null ;
}
return stringEncryptor . decrypt (encryptedData);
}
}
What Gets Encrypted
API Keys and Tokens : provider.apiKeyValue
Dynamic Auth Credentials : provider.dynamicAuthPayload
OAuth2 Secrets : Any sensitive values in custom headers or payloads
Encryption Flow
On Save (Provider or Tool Creation):
// Before saving to database
String encryptedKey = encryptionService . encrypt ( "ghp_abc123def456" );
provider . setApiKeyValue (encryptedKey); // Store encrypted value
apiProviderRepository . save (provider);
On Use (Tool Execution):
// When making API request
String decryptedKey = encryptionService . decrypt ( provider . getApiKeyValue ());
requestSpec . header ( "Authorization" , "Bearer " + decryptedKey);
Jasypt Configuration
Jasypt is configured via environment variable or application.properties:
jasypt.encryptor.password =${JASYPT_ENCRYPTOR_PASSWORD:defaultSecretKey}
jasypt.encryptor.algorithm =PBEWithMD5AndDES
Production Setup:
export JASYPT_ENCRYPTOR_PASSWORD = "your-strong-secret-key"
java -jar handsai.jar
Security Best Practice : Use a strong, unique encryption key in production. Store it securely (e.g., in a secrets manager, not in version control).
GraalVM Native Image Compatibility
Jasypt uses reflection, which requires explicit hints for GraalVM compilation. HandsAI registers these in NativeHintsConfig.java:
@ Override
public void registerHints ( RuntimeHints hints, ClassLoader classLoader) {
// Register Jasypt classes for reflection
hints . reflection (). registerType (
TypeReference . of ( "org.jasypt.encryption.pbe.StandardPBEStringEncryptor" ),
MemberCategory . INVOKE_PUBLIC_CONSTRUCTORS ,
MemberCategory . INVOKE_PUBLIC_METHODS
);
// ... other registrations
}
This ensures HandsAI can be compiled as a native executable while maintaining encryption functionality.
Authentication in Action
Here’s how authentication flows through a complete tool execution:
GET http://localhost:8080/mcp/tools/list
Response includes tools (authentication not exposed):
{
"jsonrpc" : "2.0" ,
"result" : {
"tools" : [
{
"name" : "github-create-issue" ,
"description" : "Creates a new issue in a GitHub repository." ,
"inputSchema" : { ... }
}
]
}
}
POST http://localhost:8080/mcp/tools/call
{
"jsonrpc" : "2.0",
"method" : "tools/call",
"params" : {
"name" : "github-create-issue",
"arguments" : {
"owner" : "facebook",
"repo" : "react",
"title" : "Bug in useEffect"
}
},
"id" : "req-1"
}
Step 3: Authentication Resolution
ToolExecutionService retrieves the tool and provider:
ApiTool tool = toolCacheManager . getCachedTool ( "github-create-issue" )
. orElseThrow (() -> new ResourceNotFoundException ( "Tool not found" ));
ApiProvider provider = tool . getProvider ();
Step 4: Token Retrieval
If using dynamic auth:
String token ;
if ( provider . isDynamicAuth ()) {
token = dynamicTokenManager . getToken (provider); // Fetch or use cached token
} else {
token = encryptionService . decrypt ( provider . getApiKeyValue ()); // Decrypt static credential
}
Step 5: Request Construction
Build the authenticated request:
RestClient . RequestBodySpec requestSpec = restClient
. method ( HttpMethod . POST )
. uri ( "https://api.github.com/repos/facebook/react/issues" )
. header ( "Authorization" , "Bearer " + token)
. header ( "Accept" , "application/vnd.github+json" )
. contentType ( MediaType . APPLICATION_JSON )
. body (arguments);
Step 6: API Call and Response
String response = requestSpec . retrieve (). body ( String . class );
The response is wrapped in MCP format and returned to the AI agent.
Security Considerations
Encryption at Rest All credentials encrypted with Jasypt before storage in SQLite
SSRF Protection Validates URLs to prevent internal network access
Token Caching Short-lived cache (5 min) minimizes credential exposure
No Credential Logging Credentials never appear in application logs
Best Practices
Use Environment Variables : Store JASYPT_ENCRYPTOR_PASSWORD securely
Rotate Credentials : Regularly update API keys and tokens
Enable Dynamic Auth : Prefer short-lived tokens over long-lived keys
Limit Tool Scope : Only grant minimum required permissions to API tokens
Monitor Usage : Check analytics for suspicious tool execution patterns
Real-World Authentication Examples
Here are complete authentication configurations from HandsAI’s use case library:
Resend API (Static Bearer Token)
{
"authenticationType" : "BEARER_TOKEN" ,
"apiKeyLocation" : "HEADER" ,
"apiKeyName" : "Authorization" ,
"apiKeyValue" : "re_abc123"
}
Tavily Search (API Key in Body)
{
"authenticationType" : "API_KEY" ,
"apiKeyLocation" : "IN_BODY" ,
"apiKeyName" : "api_key" ,
"apiKeyValue" : "tvly-abc123"
}
Bluesky (OAuth2 with Dynamic Auth)
{
"authenticationType" : "BEARER_TOKEN" ,
"apiKeyLocation" : "HEADER" ,
"apiKeyName" : "Authorization" ,
"isDynamicAuth" : true ,
"dynamicAuthUrl" : "https://bsky.social/xrpc/com.atproto.server.createSession" ,
"dynamicAuthMethod" : "POST" ,
"dynamicAuthPayloadType" : "JSON" ,
"dynamicAuthPayloadLocation" : "BODY" ,
"dynamicAuthPayload" : "{ \" identifier \" : \" user.bsky.social \" , \" password \" : \" app_pwd \" }" ,
"dynamicAuthTokenExtractionPath" : "accessJwt"
}
{
"authenticationType" : "API_KEY" ,
"apiKeyLocation" : "HEADER" ,
"apiKeyName" : "x-goog-api-key" ,
"apiKeyValue" : "AIza..."
}
Next Steps
MCP Protocol Learn how authentication integrates with MCP
Tool Registry Understand how tools and providers are stored
Import Use Cases Import pre-configured APIs with authentication
Security Advanced security best practices