OAuth2 and OIDC
Punk::OAuth2 covers all three sides of the protocol: "Log in with ..." as a client, validating Bearer tokens as a resource server, and issuing them as a full authorization server. Signatures run through Crypt::JWS, a 100% XS libcrypto binding.
Social login
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 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',
};
oauth2_login '/auth' mounts GET /auth/:provider (starts a login)
and GET /auth/:provider/callback (completes it), using the
authorization-code flow with PKCE (S256 only), single-use signed
state, an OIDC nonce, and id_token verification against a cached
JWKS. Presets cover google, github and generic oidc; without
one, discovery => 1 reads the issuer's discovery document
(issuer-checked and SSRF-guarded).
on_login is called as ($c, $identity, $tokens) with a normalized
identity - { provider, sub, email, email_verified, name, picture, raw } - and typically ends in
$c->login($user). Tokens are handed over and
then discarded: nothing token-shaped is ever stored in the session.
Resource server
Punk::OAuth2::Checker validates incoming Bearer tokens in XS -
signature against a JWKS or static key, then issuer, audience and
expiry - as a route guard or in an OpenAPI security map:
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',
);
under '/api' => Punk::OAuth2::Checker->guard($jwt, scopes => ['read:books']);
api 'openapi.json' => { security => { oauth => $jwt } };
Authorization server
oauth2_server '/oauth' => {
issuer => 'https://idp.example.com',
store => { dsn => 'dbi:SQLite:idp.db' },
authenticate => sub {
my ($c) = @_;
return $c->auth_id // $c->redirect('/login?to=' . $c->req->path);
},
};
One keyword serves /authorize, /token, /revoke, /introspect,
/jwks.json and the RFC 8414 metadata. Grants are authorization code
with PKCE, refresh_token with rotation, and client_credentials -
the implicit and password grants are removed as in OAuth 2.1, and not
configurable back on. Access tokens are RFC 9068 JWTs (ES256 by
default), so a resource server validates them statelessly with the
checker above; refresh tokens, codes and client secrets are opaque,
and the store only ever holds their SHA-256 digests.