Catalyst-Plugin-OpenIDConnect
view release on metacpan or search on metacpan
DEPLOYMENT.md view on Meta::CPAN
# Deployment Guide
Production deployment considerations for Catalyst::Plugin::OpenIDConnect.
## Prerequisites
- Perl 5.20 or higher
- Catalyst 5.90100 or higher
- OpenSSL for key generation
- HTTP/HTTPS web server
- Database (optional, for persistent storage)
## Installation
### 1. Install Dependencies
Using cpanm:
```bash
cpanm Catalyst::Plugin::OpenIDConnect
```
Or using cpanfile:
```bash
cpanm --installdeps .
```
### 2. Generate RSA Keys
```bash
# Generate 2048-bit RSA key pair
openssl genrsa -out /secure/path/private.pem 2048
# Extract public key
openssl rsa -in /secure/path/private.pem -pubout -out /secure/path/public.pem
# Set restrictive permissions
chmod 600 /secure/path/private.pem
chmod 644 /secure/path/public.pem
```
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
grant_types = authorization_code refresh_token
scope = openid profile email
</my-app>
</clients>
<user_claims>
sub = id
name = full_name
email = email
picture = avatar_url
</user_claims>
</Plugin::OpenIDConnect>
<Plugin::Session>
expires = 2592000
cookie_secure = 1
cookie_httponly = 1
cookie_samesite = Lax
</Plugin::Session>
```
### 4. Create the OpenIDConnect Controller
Create `lib/MyApp/Controller/OpenIDConnect.pm` in your application:
```perl
package MyApp::Controller::OpenIDConnect;
use Moose;
use namespace::autoclean;
BEGIN { extends 'Catalyst::Plugin::OpenIDConnect::Controller::Root' }
__PACKAGE__->meta->make_immutable;
1;
```
Then load it in your main app module before setup:
```perl
package MyApp;
use Catalyst qw/
OpenIDConnect
Session
Session::Store::File
Session::State::Cookie
/;
# Load the controller before setup
DEPLOYMENT.md view on Meta::CPAN
my $code_row = $self->dbic->resultset('AuthCode')->find({ code => $code });
return unless $code_row;
# Check expiration
return if DateTime->now > $code_row->expires_at;
return {
client_id => $code_row->client_id,
user => $code_row->user,
scope => $code_row->scope,
redirect_uri => $code_row->redirect_uri,
nonce => $code_row->nonce,
};
}
__PACKAGE__->meta->make_immutable;
```
## Redis Store (FastCGI and Multi-Process Deployments)
The default in-process memory store keeps authorization codes in a Perl hash
inside each worker process. Under a **FastCGI** or any other pre-forking server
this means codes created in one worker are not visible to other workers, causing
random "invalid_grant" errors at the token endpoint.
The `Catalyst::Plugin::OpenIDConnect::Utils::Store::Redis` backend solves this
by storing codes in a shared Redis instance with automatic TTL expiry.
### Installing the Redis client
Install either `Redis::Fast` (recommended â XS-based, faster) or `Redis`:
```bash
cpanm Redis::Fast
# or
cpanm Redis
```
The store will use whichever is installed, preferring `Redis::Fast`.
### Configuring the Redis store
Add `store_class` and `store_args` to your `Plugin::OpenIDConnect` config block.
**`catalyst.conf` (Apache-style):**
```
<Plugin::OpenIDConnect>
store_class = Catalyst::Plugin::OpenIDConnect::Utils::Store::Redis
<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
__PACKAGE__->config(
'Plugin::OpenIDConnect' => {
store_class => 'Catalyst::Plugin::OpenIDConnect::Utils::Store::Redis',
store_args => {
server => $ENV{REDIS_URL} // '127.0.0.1:6379',
prefix => 'myapp:oidc:code:',
code_ttl => 600,
# password => $ENV{REDIS_PASSWORD},
},
issuer => { ... },
...
},
);
```
### Redis server setup
For production, ensure:
1. **Persistence** â enable `appendonly yes` (AOF) or RDB snapshots so codes
survive a Redis restart within their TTL window.
2. **Memory limit** â set `maxmemory` and `maxmemory-policy allkeys-lru` to
prevent unbounded growth. Authorization codes are short-lived (10 min by
default) so memory usage is proportional to concurrent login traffic.
3. **Authentication** â enable `requirepass` and pass the password via
`store_args.password` (or the environment variable REDIS_PASSWORD â never hardcode it).
4. **TLS** â use Redis 6+ TLS or an stunnel/sidecar if the Redis server is not
on the same host as the application.
5. **Separate namespace** â use a unique `prefix` per application to avoid key
collisions when multiple apps share a Redis instance.
Minimal `/etc/redis/redis.conf` additions:
```
bind 127.0.0.1
requirepass <strong-random-password>
maxmemory 256mb
maxmemory-policy allkeys-lru
appendonly yes
```
### Fork-safety
The Redis connection is opened **lazily** on first use, after the parent process
has forked each worker. This means each FastCGI or pre-fork worker opens its own
independent TCP socket â there is no shared file-descriptor that would cause
interleaved reads/writes across processes.
Do **not** instantiate a store outside of a request context (e.g. at compile
time or during `POSIX::_exit` cleanup) when running under a pre-forking server.
( run in 0.585 second using v1.01-cache-2.11-cpan-13bb782fe5a )