Catalyst-Plugin-OpenIDConnect

 view release on metacpan or  search on metacpan

DEPLOYMENT.md  view on Meta::CPAN


## Database Integration

### PostgreSQL Example

```perl
package MyApp::Model::OIDC;
use Moose;
extends 'Catalyst::Plugin::OpenIDConnect::Utils::Store';

has dbic => (
    is  => 'ro',
    isa => 'Catalyst::Model::DBIC::Schema',
);

sub create_authorization_code {
    my ($self, $client_id, $user, $scope, $redirect_uri, $nonce) = @_;
    
    my $code = $self->_generate_secure_code();
    
    $self->dbic->resultset('AuthCode')->create({
        code => $code,
        client_id => $client_id,
        user_id => $user->id,
        scope => $scope,
        redirect_uri => $redirect_uri,
        nonce => $nonce,
        created_at => DateTime->now,
        expires_at => DateTime->now->add(minutes => 10),
    });
    
    return $code;
}

sub get_authorization_code {
    my ($self, $code) = @_;
    
    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 => { ... },
        ...
    },
);
```

DEPLOYMENT.md  view on Meta::CPAN

via your secrets manager. Reference it from your app config:

```perl
store_args => {
    server   => $ENV{REDIS_URL}      // '127.0.0.1:6379',
    password => $ENV{REDIS_PASSWORD} // undef,
    prefix   => 'myapp:oidc:code:',
},
```

## Monitoring

### Health Check Endpoint

```perl
sub health : Local : ActionClass('RenderView') {
    my ($self, $c) = @_;
    
    # Check database connectivity
    try {
        $c->model('DB')->schema->dbh->ping or die 'DB not responding';
    }
    catch {
        $c->response->status(503);
        $c->stash->{json} = { status => 'error', message => $_ };
        return;
    };
    
    $c->response->status(200);
    $c->stash->{json} = {
        status => 'healthy',
        timestamp => scalar gmtime(),
        version => $VERSION,
    };
}
```

### Logging Configuration

```perl
<Log4perl>
    log4perl.rootLogger = DEBUG, FileApp, FileError
    
    log4perl.appender.FileApp = Log::Log4perl::Appender::File
    log4perl.appender.FileApp.filename = /var/log/oidc/app.log
    log4perl.appender.FileApp.layout = PatternLayout
    log4perl.appender.FileApp.layout.ConversionPattern = %d %p [%c] %m%n
    
    log4perl.appender.FileError = Log::Log4perl::Appender::File
    log4perl.appender.FileError.filename = /var/log/oidc/error.log
    log4perl.appender.FileError.layout = PatternLayout
    log4perl.appender.FileError.layout.ConversionPattern = %d %p [%c] %m%n
    log4perl.appender.FileError.Threshold = ERROR
</Log4perl>
```

### Key Metrics to Monitor

- Authorization request latency
- Token exchange time
- UserInfo endpoint response time
- Authorization code expiration rate
- Session creation/destruction rate
- Error rate by endpoint
- Token verification failures
- Failed authentication attempts

## Security Best Practices

### General

1. **Always use HTTPS** - All OIDC endpoints must be over HTTPS
2. **Validate redirect URIs** - Strict matching required
3. **Use POST for sensitive data** - Never pass secrets in URLs
4. **Implement rate limiting** - Prevent brute force attacks
5. **Log security events** - Track failed attempts, suspicious activity
6. **Regular key rotation** - Rotate keys annually
7. **Monitor for vulnerabilities** - Keep Perl and dependencies updated

### Key Management

1. **Rotate keys periodically** - At least annually
2. **Keep private keys secure** - Restrict file permissions (600)
3. **Don't hardcode secrets** - Use environment variables or secure vaults
4. **Use key IDs** - Allow key rotation without breaking clients
5. **Publish public keys via JWKS** - Don't require manual download

### Session Management

1. **Use secure cookies** - Set `Secure`, `HttpOnly`, `SameSite` flags
2. **Short-lived sessions** - Default to 30 minutes
3. **CSRF protection** - Validate `state` parameter
4. **Nonce binding** - Prevent man-in-the-middle attacks
5. **Session timeout** - Clear old sessions regularly

### Client Authentication

1. **Use client secrets** - Not for public (JavaScript) clients
2. **PKCE for public clients** - RFC 7636 authorization code protection
3. **Validate redirect URIs** - Exact match required
4. **Limit client scope** - Principle of least privilege
5. **Client authentication at token endpoint** - Required

## Performance Optimization

### Caching

```perl
# Cache JWKS response (5 minutes)
sub jwks : Local {
    my ($self, $c) = @_;
    
    $c->response->header('Cache-Control' => 'public, max-age=300');
    # ... return JWKS
}

# Cache discovery (1 hour)
sub discovery : Path('/.well-known/openid-configuration') {
    my ($self, $c) = @_;
    
    $c->response->header('Cache-Control' => 'public, max-age=3600');
    # ... return discovery
}
```

### Database Indexes

```sql
CREATE INDEX idx_auth_code_code ON auth_codes(code);
CREATE INDEX idx_auth_code_expires ON auth_codes(expires_at);
CREATE INDEX idx_session_user ON sessions(user_id);
CREATE INDEX idx_session_created ON sessions(created_at);
```

### Connection Pooling

```perl
<Model::DB>
    <connect_info>
        <0>
            dbi:Pg:dbname=oidc;host=localhost
        </0>
        <1>
            postgres
        </1>
        <2>
            password
        </2>
        <3>
            {
                AutoCommit = 1
                RaiseError = 1
                PrintError = 0
                pg_enable_utf8 = 1
            }
        </3>
    </connect_info>
    <storage>
        <0>pg
        <1></1>
        <2>
            {



( run in 0.848 second using v1.01-cache-2.11-cpan-9581c071862 )