Catalyst-Plugin-OpenIDConnect

 view release on metacpan or  search on metacpan

lib/Catalyst/Plugin/OpenIDConnect.pm  view on Meta::CPAN

    GET  /.well-known/openid-configuration
    GET  /openidconnect/authorize
    POST /openidconnect/token
    GET  /openidconnect/userinfo
    GET  /openidconnect/jwks
    POST /openidconnect/logout

=cut

requires 'config', 'log', 'uri_for', 'user', 'request', 'response';

# Per-application-class storage for JWT and Store instances.
# Keyed by consuming application class name so that multiple Catalyst apps
# loaded in the same Perl interpreter each hold their own instances and cannot
# accidentally overwrite each other's JWT keys or stores (MED-3).
my %_oidc_jwt_by_class;
my %_oidc_store_by_class;

=head1 ATTRIBUTES

=head2 _oidc_jwt

JWT handler instance.

=cut

# Accessor method for JWT handler
sub _oidc_jwt {
    my ($self, $value) = @_;
    my $class = ref($self) || $self;
    if (defined $value) {
        die 'JWT handler must be an instance of Catalyst::Plugin::OpenIDConnect::Utils::JWT'
            unless ref $value && $value->isa('Catalyst::Plugin::OpenIDConnect::Utils::JWT');
        $_oidc_jwt_by_class{$class} = $value;
    }
    return $_oidc_jwt_by_class{$class};
}

=head2 _oidc_store

State and code storage.

=cut

# Accessor method for Store handler
sub _oidc_store {
    my ($self, $value) = @_;
    my $class = ref($self) || $self;
    if (defined $value) {
        die 'Store handler must implement Catalyst::Plugin::OpenIDConnect::Role::Store'
            unless ref $value && $value->DOES('Catalyst::Plugin::OpenIDConnect::Role::Store');
        $_oidc_store_by_class{$class} = $value;
    }
    return $_oidc_store_by_class{$class};
}

=head1 METHODS

=head2 setup

Catalyst setup hook - initialize the plugin. Note that this hook can effectively be blocked
in the consuming app by a similar setup method checking for configuration and deleting 
$config->{issuer} if not properly configured. This allows the consuming app to control whether 
the plugin initializes or not, which is useful for example in FastCGI deployments where multiple 
apps share the same codebase but only some are OIDC providers.

=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.
            my $store_class = $config->{store_class}
                || 'Catalyst::Plugin::OpenIDConnect::Utils::Store';
            my $store_args  = { %{ $config->{store_args} || {} } };

            # Allow the Redis password to be supplied via the environment so
            # that secrets are not embedded in application config files.
            if ( !exists $store_args->{password} && defined $ENV{REDIS_PASSWORD} && $ENV{REDIS_PASSWORD} ne '' ) {
                $store_args->{password} = $ENV{REDIS_PASSWORD};
            }

            # Dynamically load the store class (no-op if already loaded)
            require Module::Runtime;
            Module::Runtime::require_module($store_class);

            my $store = $store_class->new(
                logger => $app->log,
                %$store_args,
            );
            die "store_class '$store_class' does not implement Role::Store"
                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 {



( run in 1.753 second using v1.01-cache-2.11-cpan-800906f7e73 )