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.