Security Advisories (1)
CVE-2026-75866 (2026-08-22)

Punk::OAuth2::Server versions through 0.03 for Perl issue access tokens outside a client's registered scopes and grant types because no authorization path reads them. Punk::OAuth2::Server::Store registers scopes and grant_types per client and documents both as client registration. token dispatches on the grant_type in the request body, so a client registered for authorization_code alone can ask for client_credentials, and that arm passes the requested scope straight to the minter, which signs it into the at+jwt access token. authorize copies the query scope into the authorization code record without comparing it against the registration, leaving the optional consent hook as the only check between an arbitrary scope and the issued code. redirect_uris on the same client row is read and enforced. A registered client can obtain a correctly signed token carrying any scope it names, and a resource server running Punk::OAuth2::Checker accepts that token and honours the scope. A client registered without a secret authenticates on its client_id alone, so anyone who knows that identifier can request one.

NAME

Punk::OAuth2::Checker - resource server bearer token checkers

SYNOPSIS

use Punk::OAuth2::Checker;

my $jwt = Punk::OAuth2::Checker->jwt(
	issuer   => 'https://idp.example.com',
	jwks_url => 'https://idp.example.com/oauth/jwks.json',
	audience => 'https://api.example.com',
	algs     => ['RS256', 'ES256'],
);

# the OpenAPI security map (checker is the house contract)
$scope->api('openapi.json' => { security => { oauth => $jwt } });

# a plain route guard emitting RFC 6750 WWW-Authenticate
under '/api' => Punk::OAuth2::Checker->guard($jwt,
                                             scopes => ['read:books']);

DESCRIPTION

Validates incoming Bearer access tokens, implemented in XS. The two factories return a checker coderef matching the OpenAPI security-map contract - $checker->($credential, $c, $operationId, $scopes) returning the claims hashref or false - and guard returns a route guard. This module is the loader and the manual; the methods and the checker/guard closures are XS.

jwt (%opts)

my $jwt = Punk::OAuth2::Checker->jwt(
	issuer   => 'https://idp.example.com',
	jwks_url => 'https://idp.example.com/oauth/jwks.json',
	audience => 'https://api.example.com',
	algs     => ['RS256', 'ES256'],   # allowlist
	leeway   => 60,
);
# ...or a static key instead of jwks_url:
my $jwt = Punk::OAuth2::Checker->jwt(
	issuer => ..., audience => ..., key => $pem_or_secret);

Local JWT validation: jwks_url (keys fetched and cached via Punk::OAuth2::JWKS) or a static key (a Crypt::JWS::Key, a PEM string, or an HS shared secret). Checks the algorithm allowlist before any crypto, then the signature, then issuer (exact), audience (the aud claim must contain it), exp/nbf with leeway, and the required scopes against the scope string or scp array. Returns a checker coderef.

introspect (%opts)

my $intro = Punk::OAuth2::Checker->introspect(
	url           => 'https://idp.example.com/oauth/introspect',
	client_id     => 'my-api',
	client_secret => 's3cr3t',
	cache_ttl     => 60,
);

RFC 7662 introspection: POSTs the token to url with client credentials, requires active: true, and caches the result for cache_ttl seconds keyed by the token's SHA-256 (never the raw token). Returns a checker coderef.

guard ($checker, %opts)

under '/api' => Punk::OAuth2::Checker->guard($jwt,
	scopes => ['read:books'], realm => 'books-api');

# in a handler behind the guard:
get '/api/books' => sub {
	my ($c) = @_;
	my $sub = $c->stash->{auth}{oauth}{sub};
	...
};

Wraps a checker as a plain route guard. It extracts the Bearer credential itself and, on denial, returns a 401 (invalid_token) or 403 (insufficient_scope) with an RFC 6750 WWW-Authenticate header. scopes lists the required scopes; scheme names the stash slot (default oauth); realm sets the challenge realm.

On success the claims land in $c->stash->{auth}{$scheme}; on failure the reason is in $c->stash->{'punk.oauth2.error'}.

AUTHOR

LNATION, <email at lnation.org>

LICENSE AND COPYRIGHT

This software is Copyright (c) 2026 by LNATION <email@lnation.org>.

This is free software, licensed under:

The Artistic License 2.0 (GPL Compatible)