view release on metacpan or search on metacpan
DEPLOYMENT.md view on Meta::CPAN
Note: For production, consider using 4096-bit keys or storing keys in a HSM (Hardware Security Module).
### 3. Configure Your Application
Create/update `catalyst.conf`:
```
<Plugin::OpenIDConnect>
<issuer>
url = https://auth.example.com
private_key_file = /secure/path/private.pem
public_key_file = /secure/path/public.pem
key_id = prod-key-2024-01
</issuer>
<clients>
<my-app>
client_secret = <randomly-generated-secret>
redirect_uris = https://app.example.com/callback https://app.example.com/oauth/callback
post_logout_redirect_uris = https://app.example.com/logged-out
response_types = code
DEPLOYMENT.md view on Meta::CPAN
<store_args>
server = 127.0.0.1:6379
prefix = myapp:oidc:code:
code_ttl = 600
# password = <redis-auth-password> # omit if no AUTH required
</store_args>
<issuer>
url = https://auth.example.com
private_key_file = /secure/path/private.pem
public_key_file = /secure/path/public.pem
key_id = prod-key-2024-01
</issuer>
...
</Plugin::OpenIDConnect>
```
**Perl hash config (e.g. `MyApp.pm`):**
```perl
IMPLEMENTATION_GUIDE.md view on Meta::CPAN
- Not accessible to browser (HTTP-only cookies in production)
## Configuration
### Issuer Configuration
```perl
<Plugin::OpenIDConnect>
<issuer>
url = http://localhost:5000
private_key_file = /path/to/private.pem
public_key_file = /path/to/public.pem
key_id = my-key-123
</issuer>
```
**Fields:**
- `url` - The issuer identifier (in iss claim)
- `private_key_file` - Path to RSA private key (PEM format)
- `public_key_file` - Path to RSA public key (optional, derived from private)
- `key_id` - Key identifier for JWK Set
### Client Configuration
```perl
<clients>
<my-client>
client_secret = secret123
redirect_uris = http://app.example.com/callback
QUICKSTART.md view on Meta::CPAN
OpenIDConnect
Session
Session::Store::File
Session::State::Cookie
/;
__PACKAGE__->config(
'Plugin::OpenIDConnect' => {
issuer => {
url => 'http://localhost:5000',
private_key_file => '/path/to/private.pem',
public_key_file => '/path/to/public.pem',
key_id => 'my-key-1',
},
clients => {
'my-client' => {
client_secret => 'my-secret',
redirect_uris => ['http://localhost:3000/callback'],
post_logout_redirect_uris => ['http://localhost:3000/logged-out'],
},
},
# Load the controller before setup
use MyApp::Controller::OpenIDConnect;
```
### 3. Configure in your catalyst.conf
```
<Plugin::OpenIDConnect>
<issuer>
url = http://localhost:5000
private_key_file = /path/to/private_key.pem
public_key_file = /path/to/public_key.pem
key_id = my-key-123
</issuer>
<clients>
<MyClient>
client_id = my-client-id
client_secret = my-client-secret
redirect_uris = http://localhost:3000/callback
post_logout_redirect_uris = http://localhost:3000/logged-out
GET /.well-known/openid-configuration
```
Returns the OpenID Connect provider configuration in JSON format.
## Configuration Reference
### Issuer Configuration
- `url`: The issuer URL (used as 'iss' claim in tokens)
- `private_key_file`: Path to RSA private key for signing tokens
- `public_key_file`: Path to RSA public key for verification (auto-derived from private key if not provided)
- `key_id`: Key identifier (used in JWT header)
### Client Configuration
- `client_id`: Unique client identifier
- `client_secret`: Client secret for token endpoint
- `redirect_uris`: Arrayref or whitespace-separated string of URIs the client is permitted to redirect to after authorization. At least one entry is required.
- `post_logout_redirect_uris`: Arrayref or whitespace-separated string of URIs the client is permitted to redirect to after logout. Required when the client will use `post_logout_redirect_uri` at the logout endpoint.
- `response_types`: Space-separated response types (e.g., "code" or "code id_token")
example/app.pl view on Meta::CPAN
__PACKAGE__->config(
name => 'OIDCExample',
# Disable deprecated actions
'disable_component_resolution_regex_fallback' => 1,
# OpenID Connect configuration
'Plugin::OpenIDConnect' => {
issuer => {
url => 'http://localhost:5000',
private_key_file => 'example/keys/private.pem',
public_key_file => 'example/keys/public.pem',
key_id => 'example-key-1',
},
# Client configurations
clients => {
'example-client' => {
client_secret => 'example-client-secret',
redirect_uris => ['http://localhost:3000/callback'],
post_logout_redirect_uris => ['http://localhost:3000/logged-out'],
lib/Catalyst/Plugin/OpenIDConnect.pm view on Meta::CPAN
OpenIDConnect
Session
Session::Store::File
Session::State::Cookie
/;
MyApp->config(
'Plugin::OpenIDConnect' => {
issuer => {
url => 'http://localhost:5000',
private_key_file => '/path/to/private.pem',
public_key_file => '/path/to/public.pem',
key_id => 'key-123',
},
clients => {
'my-client' => {
client_secret => 'secret123',
redirect_uris => ['http://localhost:3000/callback'],
response_types => ['code'],
grant_types => ['authorization_code'],
scope => 'openid profile email',
lib/Catalyst/Plugin/OpenIDConnect.pm view on Meta::CPAN
=cut
after 'setup' => sub {
my ($app) = @_;
my $config = $app->config->{'Plugin::OpenIDConnect'} || {};
$app->log->debug('OpenID Connect plugin setup starting') if $config->{debug};
# Only initialize if properly configured
if ( $config->{issuer} && $config->{issuer}{private_key_file} ) {
try {
$app->log->debug('Initializing OpenID Connect with issuer: ' . $config->{issuer}{url}) if $config->{debug};
# Create JWT handler
my $jwt = $app->_oidc_build_jwt_handler($config);
$app->_oidc_jwt($jwt);
$app->log->debug('JWT handler initialized successfully') if $config->{debug};
# Create store - class and constructor args are configurable so that
# shared-memory backends (e.g. Redis) can be used under FastCGI.
lib/Catalyst/Plugin/OpenIDConnect.pm view on Meta::CPAN
unless $store->DOES('Catalyst::Plugin::OpenIDConnect::Role::Store');
$app->_oidc_store($store);
$app->log->debug("State store initialized ($store_class)") if $config->{debug};
}
catch {
$app->log->error("Failed to initialize OpenID Connect plugin: $_");
die $_;
};
} else {
$app->log->warn('OpenID Connect plugin not configured (missing issuer.private_key_file)');
}
};
before setup_finalize => sub {
my ($app) = @_;
my $config = $app->config->{'Plugin::OpenIDConnect'} || {};
$config->{debug} ||= $app->debug;
};
=head2 openidconnect()
lib/Catalyst/Plugin/OpenIDConnect.pm view on Meta::CPAN
my ( $c, $config ) = @_;
$c->log->debug('Building JWT handler from configuration') if $config->{debug};
my $issuer_cfg = $config->{issuer} || {};
my $issuer_url = $issuer_cfg->{url} || $c->uri_for('/')->as_string;
$c->log->debug("JWT issuer URL: $issuer_url") if $config->{debug};
# Load private key
my $private_key_file = $issuer_cfg->{private_key_file}
or die 'issuer.private_key_file is required';
$c->log->debug("Loading private key from: $private_key_file") if $config->{debug};
open my $key_fh, '<', $private_key_file
or die "Cannot read private key file: $!";
my $key_data = do { local $/; <$key_fh> };
close $key_fh;
my $private_key = Crypt::OpenSSL::RSA->new_private_key($key_data);
$c->log->debug('Private key loaded successfully') if $config->{debug};
# Load or derive public key
my $public_key;
if ( my $public_key_file = $issuer_cfg->{public_key_file} ) {
$c->log->debug("Loading public key from: $public_key_file") if $config->{debug};
open $key_fh, '<', $public_key_file
or die "Cannot read public key file: $!";
my $pub_data = do { local $/; <$key_fh> };
close $key_fh;
$public_key = Crypt::OpenSSL::RSA->new_public_key($pub_data);
$c->log->debug('Public key loaded successfully') if $config->{debug};
} else {
$c->log->debug('Deriving public key from private key') if $config->{debug};
# Extract public key from private key
$public_key = Crypt::OpenSSL::RSA->new_public_key(
$private_key->get_public_key_string()
);
$c->log->debug('Public key derived successfully') if $config->{debug};
}
my $key_id = $issuer_cfg->{key_id} || 'default';
$c->log->debug("Using key ID: $key_id") if $config->{debug};
return Catalyst::Plugin::OpenIDConnect::Utils::JWT->new(
private_key => $private_key,
public_key => $public_key,
key_id => $key_id,
issuer => $issuer_url,
logger => $c->log,
);
}
1;
=head1 AUTHOR
lib/Catalyst/Plugin/OpenIDConnect/Context.pm view on Meta::CPAN
Returns the JWT handler instance.
=cut
sub jwt {
my ($self) = @_;
$self->catalyst->log->debug('Retrieving JWT handler') if $self->config->{debug};
my $jwt = $self->catalyst->_oidc_jwt();
unless ($jwt) {
$self->catalyst->log->error('OpenID Connect JWT handler not initialized');
die 'OpenID Connect JWT handler not initialized. Check your Plugin::OpenIDConnect configuration (issuer.private_key_file and issuer.public_key_file required).';
}
return $jwt;
}
=head2 store()
Returns the state store instance.
=cut
lib/Catalyst/Plugin/OpenIDConnect/Utils/JWT.pm view on Meta::CPAN
Catalyst::Plugin::OpenIDConnect::Utils::JWT - JWT handling for OpenID Connect
=head1 DESCRIPTION
Provides JWT signing and verification functionality using RS256 (RSA SHA-256) algorithm
for OpenID Connect token creation and validation.
=head1 ATTRIBUTES
=head2 private_key
The RSA private key for signing tokens.
=cut
has private_key => (
is => 'ro',
isa => 'Crypt::OpenSSL::RSA',
required => 1,
);
=head2 public_key
The RSA public key for verifying tokens.
=cut
lib/Catalyst/Plugin/OpenIDConnect/Utils/JWT.pm view on Meta::CPAN
$payload{$_} = int( $payload{$_} )
for grep { defined $payload{$_} } qw(exp iat nbf);
my $payload_json = encode_json( \%payload );
my $header_b64 = _urlsafe_b64_encode($header_json);
my $payload_b64 = _urlsafe_b64_encode($payload_json);
# Create signature (explicitly use SHA256 for RS256)
my $signing_input = "$header_b64.$payload_b64";
my $priv_key = $self->private_key;
# Ensure consistent RSA configuration for signing (RFC 3447 PKCS1v15)
$priv_key->use_pkcs1_padding();
$priv_key->use_sha256_hash();
my $signature = $priv_key->sign($signing_input);
my $signature_b64 = _urlsafe_b64_encode($signature);
my $token = "$signing_input.$signature_b64";
if ( $self->logger ) {
$self->logger->debug( sprintf(
use Crypt::OpenSSL::RSA;
use JSON::MaybeXS qw(encode_json);
use MIME::Base64 qw(encode_base64);
# ---------------------------------------------------------------------------
# Test fixtures
# ---------------------------------------------------------------------------
my $rsa = Crypt::OpenSSL::RSA->generate_key(1024);
my $private_key = $rsa;
my $public_key = Crypt::OpenSSL::RSA->new_public_key(
$rsa->get_public_key_string()
);
my $jwt = Catalyst::Plugin::OpenIDConnect::Utils::JWT->new(
private_key => $private_key,
public_key => $public_key,
key_id => 'test-key',
issuer => 'http://localhost:5000',
);
ok($jwt, 'JWT handler created');
# ---------------------------------------------------------------------------
# Helper: build a validly-signed JWT with exactly the given payload.
# Unlike sign_token(), this does NOT auto-set iss/iat so we can test
sub _raw_jwt {
my (%payload) = @_;
my $encode = sub {
my $b64 = encode_base64($_[0], '');
$b64 =~ tr|+/=|-_|d;
return $b64;
};
my $header = $encode->(encode_json({ alg => 'RS256', typ => 'JWT', kid => 'test-key' }));
my $body = $encode->(encode_json(\%payload));
my $signing_in = "$header.$body";
$private_key->use_sha256_hash();
my $sig = $encode->($private_key->sign($signing_in));
return "$signing_in.$sig";
}
# ---------------------------------------------------------------------------
# Basic signing and verification (happy path)
# ---------------------------------------------------------------------------
my %payload = (
sub => 'user-123',
name => 'Test User',
{
{
package CapturingLogger;
sub new { bless { msgs => [] }, shift }
sub debug { push @{ $_[0]{msgs} }, $_[1] }
}
my $cap_logger = CapturingLogger->new();
my $logging_jwt = Catalyst::Plugin::OpenIDConnect::Utils::JWT->new(
private_key => $private_key,
public_key => $public_key,
key_id => 'test-key',
issuer => 'http://localhost:5000',
logger => $cap_logger,
);
$logging_jwt->sign_token(
sub => 'uid-42',
aud => 'my-client',
exp => time() + 3600,
t/03_plugin.t view on Meta::CPAN
use Catalyst::Plugin::OpenIDConnect::Context;
use Catalyst::Plugin::OpenIDConnect::Utils::JWT;
use Catalyst::Plugin::OpenIDConnect::Utils::Store;
use Crypt::OpenSSL::RSA;
use File::Temp qw(tempfile);
use JSON::MaybeXS qw(encode_json);
# Generate test keys
my $rsa = Crypt::OpenSSL::RSA->generate_key(1024);
my $private_key = $rsa;
my $public_key = Crypt::OpenSSL::RSA->new_public_key(
$rsa->get_public_key_string()
);
# Create JWT handler for testing context methods
my $jwt = Catalyst::Plugin::OpenIDConnect::Utils::JWT->new(
private_key => $private_key,
public_key => $public_key,
key_id => 'test-key',
issuer => 'http://localhost:5000',
);
ok($jwt, 'JWT handler created');
# Create store for testing context methods
my $store = Catalyst::Plugin::OpenIDConnect::Utils::Store->new();
ok($store, 'Store created');
t/03_plugin.t view on Meta::CPAN
# MED-3: JWT and Store instances are isolated per consuming application class
# ---------------------------------------------------------------------------
{
my $app_a = bless {}, 'FakeAppAlpha';
my $app_b = bless {}, 'FakeAppBeta';
# Create a second distinct JWT instance for app_b
my $rsa_b = Crypt::OpenSSL::RSA->generate_key(1024);
my $jwt_b = Catalyst::Plugin::OpenIDConnect::Utils::JWT->new(
private_key => $rsa_b,
public_key => Crypt::OpenSSL::RSA->new_public_key( $rsa_b->get_public_key_string() ),
key_id => 'key-b',
issuer => 'http://b.example.com',
);
Catalyst::Plugin::OpenIDConnect::_oidc_jwt( $app_a, $jwt );
Catalyst::Plugin::OpenIDConnect::_oidc_jwt( $app_b, $jwt_b );
is(
Catalyst::Plugin::OpenIDConnect::_oidc_jwt($app_a), $jwt,
t/05_logout.t view on Meta::CPAN
use Catalyst::Plugin::OpenIDConnect::Utils::JWT;
use Catalyst::Plugin::OpenIDConnect::Controller::Root;
use Crypt::OpenSSL::RSA;
# ---------------------------------------------------------------------------
# Test fixtures
# ---------------------------------------------------------------------------
my $rsa = Crypt::OpenSSL::RSA->generate_key(1024);
my $private_key = $rsa;
my $public_key = Crypt::OpenSSL::RSA->new_public_key( $rsa->get_public_key_string() );
my $jwt = Catalyst::Plugin::OpenIDConnect::Utils::JWT->new(
private_key => $private_key,
public_key => $public_key,
key_id => 'test-key',
issuer => 'https://auth.example.com',
);
# ---------------------------------------------------------------------------
# JWT::decode_id_token_hint â valid (non-expired) token
# ---------------------------------------------------------------------------
my $id_token = $jwt->create_id_token(
t/05_logout.t view on Meta::CPAN
is( $jwt->decode_id_token_hint($tampered_token), undef,
'decode_id_token_hint: returns undef for a tampered token' );
# ---------------------------------------------------------------------------
# JWT::decode_id_token_hint â token from a different key must be rejected
# ---------------------------------------------------------------------------
my $other_rsa = Crypt::OpenSSL::RSA->generate_key(1024);
my $other_jwt = Catalyst::Plugin::OpenIDConnect::Utils::JWT->new(
private_key => $other_rsa,
public_key => Crypt::OpenSSL::RSA->new_public_key( $other_rsa->get_public_key_string() ),
key_id => 'other-key',
issuer => 'https://auth.example.com',
);
my $foreign_token = $other_jwt->create_id_token(
sub => 'user-789',
aud => 'test-client',
exp => time() + 3600,
);