NAME
Punk::Plugin::OAuth2 - social login for Punk applications
VERSION
Version 0.05
SYNOPSIS
package MyApp;
use Punk;
use Punk::Plugin::OAuth2;
plugin 'OAuth2';
session secret => secret('session_key'), expires => '7d';
oauth2 google => {
preset => 'google',
client_id => secret('oauth.google_id'),
client_secret => secret('oauth.google_secret'),
};
oauth2 github => {
preset => 'github',
client_id => secret('oauth.github_id'),
client_secret => secret('oauth.github_secret'),
};
oauth2 corp => {
issuer => 'https://idp.corp.example',
discovery => 1,
client_id => '...',
client_secret => '...',
};
oauth2_login '/auth' => {
on_login => 'Auth#on_login',
base_url => 'https://app.example.com',
};
package MyApp::Controller::Auth;
sub on_login {
my ($c, $identity, $tokens) = @_;
$c->session->{user} = { id => find_user($identity)->id };
return; # falls through to the post-login redirect
}
DESCRIPTION
OAuth2 and OpenID Connect for Punk. The plugin installs three keywords covering both sides of the protocol:
oauth2+oauth2_login- the client (social login): authorization-code flow with PKCE (S256 only), single-use signed state, OIDC nonce, id_token verification through Crypt::JWS and a cached JWKS, and normalized identities across providers.oauth2_server- a full authorization server (see Punk::OAuth2::Server).
For the resource-server side (validating incoming Bearer tokens as route guards or in an OpenAPI security map) see Punk::OAuth2::Checker.
Declaring a provider records configuration; plugin 'OAuth2' builds providers and mounts routes - either ordering of keyword and plugin works, and mismatches croak at to_app.
oauth2_login '/auth' mounts GET /auth/:provider (starts a login) and GET /auth/:provider/callback (completes it). Flow state lives in the signed session - at most three concurrent login attempts, each valid for ten minutes, deleted on use. Tokens are handed to on_login and then discarded: nothing token-shaped is ever stored in the session.
KEYWORDS
oauth2 NAME => \%config
Declares a provider. preset is google, github, or oidc; every preset field can be overridden. Without a preset, set issuer plus discovery => 1 (endpoints from the OIDC discovery document, issuer-checked and SSRF-guarded) or explicit authorization_endpoint/token_endpoint. Other options:
- client_id / client_secret
-
The registration with the provider.
client_secretmay be omitted for a public client (public => 1) - PKCE still applies. - scope
-
Space-separated scope string; presets carry sensible defaults.
- auth_method
-
basic(default) orbody- how client credentials reach the token endpoint. - algs
-
id_token signature allowlist, default
['RS256', 'ES256']. - allow_local
-
Relax the SSRF guard for loopback IdPs (development and tests).
- ua
-
A Fetch-compatible agent or a coderef returning one - the test seam; defaults to
$c->ua.
oauth2_login PATH => \%options
Mounts the login routes. Options:
- on_login (required)
-
Coderef or
'Controller#method', called as($c, $identity, $tokens)after a verified login.$identityis{ provider, sub, email, email_verified, name, picture, raw };$tokensis a Punk::OAuth2::Tokens. A returned reference becomes the response; otherwise the user is redirected to the flow'sreturndestination orredirect_ok. - base_url
-
The application's external origin, used to build exact redirect URIs. Required in production;
trust_proxy => 1derives it from X-Forwarded-Proto/Host instead when running behind a trusted proxy. - redirect_ok
-
Default post-login destination (
/). A?return=parameter on the initiation URL overrides it when it is a same-origin relative path - absolute and protocol-relative destinations are ignored. - on_error
-
Optional handler for failed logins; the default is a plain 400 page. Details go to the log, never the browser.
oauth2_server PATH => \%options
Mounts an OAuth2 / OpenID Connect authorization server at PATH: the authorize, token, revoke, introspect, and jwks.json endpoints, plus the RFC 8414 metadata at /.well-known/oauth-authorization-server and /.well-known/openid-configuration (registered at the app root). The token, revoke, and introspect POSTs are client-authenticated and are mounted CSRF-exempt.
oauth2_server '/oauth' => {
issuer => 'https://idp.example.com',
store => { dsn => 'dbi:SQLite:idp.db' },
authenticate => 'Auth#require_user',
consent => 'Auth#consent',
};
Grants: authorization code with PKCE (S256 only), refresh_token (with rotation and family revocation on reuse), and client_credentials. Access tokens are ES256 JWTs. See Punk::OAuth2::Server for the full behaviour. Options:
- issuer (required)
-
The issuer URL; it appears in tokens and metadata and must match what resource servers expect.
- store (required)
-
A Punk::OAuth2::Server::Store, a
{ dsn =... }> hashref (built into one for you), or any object satisfying the store contract - see "CREATING YOUR OWN STORE" in Punk::OAuth2::Server::Store. - authenticate (required)
-
Coderef or
'Controller#method', called with the context; returns the authenticated user id, or a reference (a login redirect) that short-circuits the authorization request.sub require_user { my ($c) = @_; return $c->session->{user_id} if $c->session->{user_id}; return $c->redirect('/login?to=' . ...); # a reference wins } - consent
-
Optional. Called as
($c, $client, \@scopes); returns 1 (approve), 0 (deny), or a reference (render a consent page). Approvals are persisted so returning users skip consent. - alg, at_ttl, rt_ttl, key
-
Signing algorithm (default
ES256), access- and refresh-token lifetimes (seconds), and an explicit signing key (a Crypt::JWS::Key; one is generated otherwise).oauth2_server '/oauth' => { issuer => ..., store => ..., authenticate => ..., alg => 'ES256', at_ttl => 600, rt_ttl => 30 * 86400, };
Both keyword and plugin orderings work, and inline declaration via plugin 'OAuth2' => { server => {...} } is equivalent to the keyword.
CONTEXT HELPERS
The plugin installs these on the context for use inside handlers.
oauth2_authurl
get '/signin' => sub {
my ($c) = @_;
my $url = $c->oauth2_authurl('google', return_to => '/dashboard');
return $c->render(template => 'signin', google_url => $url);
};
Builds the authorization URL for a provider (for hand-rolled sign-in buttons) and records the login flow in the session, so its callback can complete just like oauth2_login's own route. return_to is an optional post-login destination.
oauth2_refresh
my $fresh = $c->oauth2_refresh('google', $tokens);
# or with a raw refresh-token string:
my $fresh = $c->oauth2_refresh('google', $refresh_token);
Performs a refresh-token grant against the provider and returns a fresh Punk::OAuth2::Tokens.
oauth2_provider
my $provider = $c->oauth2_provider('google');
my $url = $provider->authorize_url(...);
Returns the configured Punk::OAuth2::Provider object for advanced use.
SECURITY NOTES
PKCE is always on and S256 only. State is 256 random bits, single-use, ten-minute lifetime. The OIDC nonce is checked against the id_token in constant time. id_token verification enforces the algorithm allowlist, issuer equality, audience (and azp with multiple audiences), and expiry with 60 seconds of leeway. Discovery and JWKS URLs must be https and resolve to public addresses unless allow_local is set. The ?return= destination is constrained to same-origin relative paths. Provider tokens are never written to the session.
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)