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)