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::Tokens - a token-endpoint response

SYNOPSIS

sub on_login {
	my ($c, $identity, $tokens) = @_;
	$tokens->access_token;    # the access token string
	$tokens->refresh_token;   # undef unless one was issued
	$tokens->id_claims;       # verified id_token claims (OIDC)
	return unless $tokens->expired;
}

DESCRIPTION

The object handed to on_login and returned by $c->oauth2_refresh, implemented in XS. expires_in from the wire is converted to an absolute expires_at at construction.

CONSTRUCTORS

new

my $tokens = Punk::OAuth2::Tokens->new(
	access_token  => $at,
	token_type    => 'Bearer',
	expires_in    => 3600,       # becomes expires_at = now + 3600
	refresh_token => $rt,
);

from_response

my $tokens = Punk::OAuth2::Tokens->from_response($decoded_json);

Builds a Tokens object from a decoded token-endpoint response hash, keeping the original under "raw".

ACCESSORS

access_token

my $at = $tokens->access_token;

token_type

my $type = $tokens->token_type;   # 'Bearer'

refresh_token

my $rt = $tokens->refresh_token;   # undef if none was issued

id_token

my $jwt = $tokens->id_token;   # the raw OIDC id_token (OIDC only)

id_claims

my $claims = $tokens->id_claims;   # verified id_token claims hashref
my $sub    = $tokens->id_claims->{sub};

The claims of a verified OIDC id_token (set by the client flow after verification). Also a setter: $tokens->id_claims(\%claims).

scope

my $scope = $tokens->scope;   # the granted scope string, or undef

expires_at

my $epoch = $tokens->expires_at;   # absolute expiry, or undef

raw

my $hash = $tokens->raw;   # the decoded response, untouched

PREDICATES

expired

$tokens->expired;        # true if within 30s of expiry (default)
$tokens->expired(0);     # exact expiry, no leeway

True when the access token has expired (a leeway in seconds, default 30, is subtracted so a token about to expire counts as expired).

refreshable

if ($tokens->refreshable) {
	$tokens = $c->oauth2_refresh('google', $tokens);
}

True when a refresh token is present.

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)