NAME
Langertha::Knarr::Authorizer::Token - One short-lived token per model, checked against a token store
VERSION
version 1.103
SYNOPSIS
use Langertha::Knarr::Authorizer::Token;
use Langertha::Knarr::TokenStore::Memory;
use Langertha::Knarr::Handler::TokenRoutes;
my $store = Langertha::Knarr::TokenStore::Memory->new;
$store->add( token => 'tok-1', model => 'gpt-4o', provider => 'openai',
expires => time + 900 );
my $knarr = Langertha::Knarr->new(
authorizer => Langertha::Knarr::Authorizer::Token->new( store => $store ),
handler => Langertha::Knarr::Handler::TokenRoutes->new( routes => {
openai => { provider => 'openai', key_env => 'OPENAI_API_KEY' },
} ),
);
DESCRIPTION
The Langertha::Knarr::Authorizer behind token_auth: a platform issues a short-lived token for exactly one model, and Knarr checks it against a Langertha::Knarr::TokenStore (a database view of the issued tokens).
Identify
The token comes as Authorization: Bearer token or x-api-key; a header sent twice counts as one value of comma-separated elements, like the proxy key (an Authorization element without Bearer is no token). No token, or more than one distinct token, is 401. The token's "hash_token" in Langertha::Knarr::TokenStore is looked up; an unknown, revoked or expired token is 401 unauthorized, the same text as a wrong proxy key. The A2A agent card stays anonymous.
Hits are cached by hash. A cached row is used only while its expires is in the future, and the cache is dropped whole whenever the store calls its "on_invalidate" in Langertha::Knarr::TokenStore (a notification on the database's channel, a lost connection), so a revocation takes effect without a restart. A lookup that was in flight when the cache was dropped answers its request but is not cached. Misses are not cached.
When a lookup fails -- the store is not "available" in Langertha::Knarr::TokenStore (connection lost, reconnecting) or broke down -- a request with a token is refused with 503 token check unavailable: fail-closed, but not 401, so a platform does not take a valid token for a bad one and the client may try again. The answer says nothing about the token; a request without one still gets 401. A cached row is not used either while the store is not available. The store connects when the serving process calls "start", else on its first lookup.
The synchronous path (Langertha::Knarr::PSGI) asks the store for every request instead of using the cache: the store's blocking lookup is what picks up its pending notifications.
The identity is { model, provider, expires } from the token's row, never the token or its hash. Nothing here logs either; a refusal is logged at debug level as token check: ... with its reason only.
Authorize
With "match_model" (the default) a chat request and POST /api/show must name the token's model, else 403 forbidden: model not allowed for this token (no model name in the text). A request that names no model (A2A, ACP without agent_name) has been given the token's model by then ("apply_grant" in Langertha::Knarr::Request). The model lists (/v1/models, /api/tags, ACP /agents) show the token's model only.
The provider manifest (/.well-known/langertha.json) is refused with 403 forbidden: not available with token auth: it describes the whole model surface of the Knarr, which a token for one model must not see, and its build is shared by every client. /api/version needs a valid token and nothing more.
"forwards_client_key" is false: a token-checked request never goes through the raw passthrough, and Authorization and x-api-key are dropped from every header set Knarr forwards, so the token never reaches an upstream.
store
Required. A Langertha::Knarr::TokenStore. The authorizer sets its "on_invalidate" in Langertha::Knarr::TokenStore: a store serves one authorizer.
match_model
Default true. False lets a token use any model (the lists still show only its own).
start
$authorizer->start($loop);
Starts the store ("start" in Langertha::Knarr::TokenStore) the first time it is called in a process; later calls in the same process do nothing. Langertha::Knarr calls it in the process that serves, once its loop is set up, so the store's connection is under way before the first request -- "start" in Langertha::Knarr::TokenStore::Pg returns before its connect attempt is done. A forked worker is a new process and starts its own.
clear_cache
Drops every cached row. The store's invalidation calls it.
identify_f
my $result = await $authorizer->identify_f( $http_req, $action );
{ identity => { model, provider, expires } }, or { status => 401, message => 'unauthorized' }, or { status => 503, message => 'token check unavailable' }; the agent card gets { identity => {} }. Never fails.
identify
my $result = $authorizer->identify( $http_req, $action );
The same result, with the store's blocking "lookup" in Langertha::Knarr::TokenStore for every request (no cache).
authorize
my ( $status, $message ) = $authorizer->authorize( $identity, $action, $model );
403 for the manifest, and with "match_model" for a chat or show whose model is not the token's; empty otherwise.
visible_models
my $models = $authorizer->visible_models( $identity, $listed );
[ { id => $token_model, object => 'model' } ], whatever the handler lists; empty for an identity without a model.
forwards_client_key
False: the token never goes upstream.
SUPPORT
Issues
Please report bugs and feature requests on GitHub at https://github.com/Getty/langertha-knarr/issues.
IRC
Join #langertha on irc.perl.org or message Getty directly.
CONTRIBUTING
Contributions are welcome! Please fork the repository and submit a pull request.
AUTHOR
Torsten Raudssus <torsten@raudssus.de> https://raudssus.de/
COPYRIGHT AND LICENSE
This software is copyright (c) 2026 by Torsten Raudssus.
This is free software; you can redistribute it and/or modify it under the same terms as the Perl 5 programming language system itself.