Remote token fetcher
Similar to the "Biscuit to user" and "apikey bridge" plugins, this plugin retrieves a Biscuit token based on the request context (HTTP resource, connected user, used API key, etc.). The token is retrieved via an HTTP call, and the remote server is responsible for generating the token. This token is then re-injected into the current HTTP request or made available in Otoroshi's expression language for a future step in Otoroshi's plugin pipeline.
Configuration
The plugin requires a minimal configuration to function properly. Below is a sample configuration:
{
"api_url": "http://foo.bar",
"api_method": "POST",
"api_headers": {
"Content-Type": "application/json",
"Authorization": "Bearer ${user.tokens.access_token}"
},
"api_timeout": 10000,
"api_tls_config": { // mTLS config
"certs": [],
"trusted_certs": [],
"enabled": false,
"loose": false,
"trust_all": false
},
"token_replace_loc": "header",
"token_replace_name": "Authorization",
"token_resp_loc": "token",
"otoroshi_ctx": false // inject otoroshi context in the http body
}
Demo
the code for the server that return a biscuit can be the following:
const http = require('http');
const PORT = 3214;
const server = http.createServer((req, res) => {
if (req.method !== 'POST' || req.url !== '/biscuit') {
res.writeHead(404, { 'Content-Type': 'application/json' });
return res.end(JSON.stringify({ error: 'Not found' }));
}
let body = '';
req.on('data', chunk => { body += chunk; });
req.on('end', () => {
// Réponse JSON
const payload = { biscuit: 'EpIBCigKB21hdGhpZXUKBG5hbWUYAyIJCgcIDhIDGIAIIgoKCAiBCBIDGIAIEiQIABIgCV15zxTAdpClBP3QLjdUEmrrXFsdEWq2-_wHWzfXMSUaQLLIaxZgEPQVD1q3H75dkalbp7bgZHLiDAPRMQwkIZ0J0cCf4oJJEEvg4u72EYveuvPvS6FCQ5osP_yRB72VeQIiIgoguLVPQPF0tnYnrJt6VAM3dzi7cyUJdnw3tnaenDRLSDs=' };
const json = JSON.stringify(payload);
res.writeHead(200, {
'Content-Type': 'application/json',
'Content-Length': Buffer.byteLength(json)
});
res.end(json);
});
});
server.listen(PORT, () => {
console.log(`Server listening on http://localhost:${PORT}`);
});
now create a route in otoroshi with the following configuration:
curl -X POST \
-H 'Content-Type: application/json' \
-H 'Authorization: Basic xxxx' \
"http://otoroshi-api.oto.tools:8080/apis/proxy.otoroshi.io/v1/routes" \
-d '
{
"_loc": {
"tenant": "default",
"teams": [
"default"
]
},
"id": "route_a2d39581b-ce95-4920-b397-54ee07870949",
"name": "test biscuit plugins",
"description": "A new route",
"tags": [],
"metadata": {
"created_at": "2025-09-24T09:53:31.665+02:00"
},
"enabled": true,
"debug_flow": false,
"export_reporting": false,
"capture": false,
"groups": [
"default"
],
"bound_listeners": [],
"frontend": {
"domains": [
"remotebiscuit.oto.tools"
],
"strip_path": true,
"exact": false,
"headers": {},
"cookies": {},
"query": {},
"methods": []
},
"backend": {
"targets": [
{
"id": "target_1",
"hostname": "request.otoroshi.io",
"port": 443,
"tls": true,
"weight": 1,
"backup": false,
"predicate": {
"type": "AlwaysMatch"
},
"protocol": "HTTP/1.1",
"ip_address": null,
"tls_config": {
"certs": [],
"trusted_certs": [],
"enabled": false,
"loose": false,
"trust_all": false
}
}
],
"root": "/",
"rewrite": false,
"load_balancing": {
"type": "RoundRobin"
},
"client": {
"retries": 1,
"max_errors": 20,
"retry_initial_delay": 50,
"backoff_factor": 2,
"call_timeout": 30000,
"call_and_stream_timeout": 120000,
"connection_timeout": 10000,
"idle_timeout": 60000,
"global_timeout": 30000,
"sample_interval": 2000,
"proxy": {},
"custom_timeouts": [],
"cache_connection_settings": {
"enabled": false,
"queue_size": 2048
}
},
"health_check": {
"enabled": false,
"url": "",
"timeout": 5000,
"healthyStatuses": [],
"unhealthyStatuses": [],
"blockOnRed": false
}
},
"backend_ref": null,
"plugins": [
{
"enabled": true,
"debug": false,
"plugin": "cp:otoroshi.next.plugins.OverrideHost",
"include": [],
"exclude": [],
"config": {},
"bound_listeners": [],
"nodeId": "cp:otoroshi.next.plugins.OverrideHost",
"plugin_index": {
"transform_request": 0
}
},
{
"plugin_index": {
"transform_request": 1
},
"nodeId": "cp:otoroshi_plugins.com.cloud.apim.otoroshi.extensions.biscuit.plugins.BiscuitRemoteTokenFetcherPlugin-0",
"plugin": "cp:otoroshi_plugins.com.cloud.apim.otoroshi.extensions.biscuit.plugins.BiscuitRemoteTokenFetcherPlugin",
"enabled": true,
"debug": false,
"include": [],
"exclude": [],
"bound_listeners": [],
"config": {
"api_url": "http://localhost:3214/biscuit",
"api_method": "POST",
"api_headers": {
"Content-Type": "application/json",
"Authorization": "Bearer ${user.tokens.access_token}"
},
"api_timeout": 10000,
"api_tls_config": {
"certs": [],
"trusted_certs": [],
"enabled": false,
"loose": false,
"trust_all": false
},
"token_replace_loc": "header",
"token_replace_name": "Authorization",
"token_resp_loc": "biscuit",
"otoroshi_ctx": true
}
},
{
"plugin_index": {
"transform_request": 2
},
"nodeId": "cp:otoroshi.next.plugins.AdditionalHeadersIn-0",
"plugin": "cp:otoroshi.next.plugins.AdditionalHeadersIn",
"enabled": true,
"debug": false,
"include": [],
"exclude": [],
"bound_listeners": [],
"config": {
"headers": {
"User-Biscuit": "${ctx.remote_fetched_biscuit}"
}
}
},
{
"plugin_index": {
"validate_access": 0
},
"nodeId": "cp:otoroshi.next.plugins.AuthModule-0",
"plugin": "cp:otoroshi.next.plugins.AuthModule",
"enabled": true,
"debug": false,
"include": [],
"exclude": [],
"bound_listeners": [],
"config": {
"pass_with_apikey": false,
"auth_module": null,
"module": "auth_mod_dev_1351bf36-ce8e-49f2-80a3-f82ea01b9468"
}
}
],
"kind": "proxy.otoroshi.io/Route"
}'
and then test it in your browser by going to http://remotebiscuit.oto.tools:8080
et the result should look like:
{
"method": "GET",
"path": "/",
"headers": {
"host": "request.otoroshi.io",
"accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7",
"referer": "http://privateapps.oto.tools:8080/",
"connection": "keep-alive",
"user-agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Safari/537.36",
"user-biscuit": "EpIBCigKB21hdGhpZXUKBG5hbWUYAyIJCgcIDhIDGIAIIgoKCAiBCBIDGIAIEiQIABIgCV15zxTAdpClBP3QLjdUEmrrXFsdEWq2-_wHWzfXMSUaQLLIaxZgEPQVD1q3H75dkalbp7bgZHLiDAPRMQwkIZ0J0cCf4oJJEEvg4u72EYveuvPvS6FCQ5osP_yRB72VeQIiIgoguLVPQPF0tnYnrJt6VAM3dzi7cyUJdnw3tnaenDRLSDs=",
"authorization": "EpIBCigKB21hdGhpZXUKBG5hbWUYAyIJCgcIDhIDGIAIIgoKCAiBCBIDGIAIEiQIABIgCV15zxTAdpClBP3QLjdUEmrrXFsdEWq2-_wHWzfXMSUaQLLIaxZgEPQVD1q3H75dkalbp7bgZHLiDAPRMQwkIZ0J0cCf4oJJEEvg4u72EYveuvPvS6FCQ5osP_yRB72VeQIiIgoguLVPQPF0tnYnrJt6VAM3dzi7cyUJdnw3tnaenDRLSDs=",
"cache-control": "max-age=0",
"accept-language": "fr-FR,fr;q=0.9,en-US;q=0.8,en;q=0.7,ca;q=0.6,la;q=0.5",
"upgrade-insecure-requests": "1",
"accept-encoding": "gzip, deflate",
"x-forwarded-for": "45.80.20.1",
"forwarded": "proto=https;for=45.80.20.1:52657;by=91.208.207.218",
"x-forwarded-port": "443",
"x-forwarded-proto": "https",
"sozu-id": "01K5XFSSE18QGYMZ6NAKG4F4BQ"
},
"cookies": {},
"body": null
}