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 )