# Changelog

All notable changes to this project will be documented in this file.

## [0.08] - 2026-04-29 (Security Fix: MED-6)

### Security

- **MED-6 fixed — Missing HTTP security headers on all responses**
  (`Controller::Root`).  A `begin : Private` action now runs before every OIDC
  endpoint and sets five response headers:
  - `Cache-Control: no-store` — mandatory per RFC 6749 §5.1 on token responses;
    applied globally so future endpoints cannot accidentally omit it.
  - `Pragma: no-cache` — HTTP/1.0 compatibility.
  - `X-Content-Type-Options: nosniff` — prevents MIME-type sniffing on all
    OIDC responses.
  - `X-Frame-Options: DENY` — guards the authorization endpoint HTML page
    against clickjacking.
  - `Content-Security-Policy: frame-ancestors 'none'` — modern equivalent of
    `X-Frame-Options` for browsers that support CSP Level 2+.

## [0.07] - 2026-04-29 (Security Fix: MED-1)

### Security

- **MED-1 fixed — Non-revocable refresh tokens** (`Controller::Root`,
  `Utils::Store`, `Utils::Store::Redis`, `Role::Store`).  Refresh tokens are
  now bound to a unique JTI (UUID v4) that is stored server-side with a TTL
  matching the 30-day token lifetime.  On every use the JTI is atomically
  consumed from the store (Perl `delete` for the in-memory backend;
  Redis `GETDEL` for the Redis backend) and a new JTI + refresh token are
  issued immediately (refresh token rotation).  A second attempt to use the
  same refresh token receives `invalid_grant`.  On logout, all outstanding JTIs
  for the user are deleted via a secondary per-subject index, invalidating any
  stolen tokens immediately.  The `Role::Store` interface gains three new
  required methods: `store_refresh_token`, `consume_refresh_token`, and
  `revoke_refresh_tokens_for_user`.

## [0.06] - 2026-04-29 (Security Fixes: MED-2, MED-3, MED-4, MED-5)

### Security

- **MED-2 fixed — Sensitive claims logged at debug level** (`Utils::JWT`).
  `sign_token` previously serialised the entire JWT payload (which may include
  PII such as email addresses, names, and user identifiers) to the debug log.
  The log statement now emits only non-sensitive metadata: `sub`, `aud`, and
  `exp`. PII-bearing claims are never written to any log output.

- **MED-3 fixed — Package-level global state shared across application classes**
  (`Catalyst::Plugin::OpenIDConnect`). The JWT handler and store were stored in
  `our $_oidc_jwt_instance` / `our $_oidc_store_instance` — package-level
  globals shared across all Catalyst applications in the same interpreter.
  These are replaced by per-application-class lexical hashes
  (`%_oidc_jwt_by_class`, `%_oidc_store_by_class`) keyed by the consuming
  class name (`ref($self) || $self`). Multiple Catalyst apps in the same process
  now each hold their own isolated JWT and store instances.

- **MED-4 fixed — Implicit grant/response types advertised in discovery**
  (`Context`). The discovery document listed `implicit` in
  `grant_types_supported` and `id_token`/`token` values in
  `response_types_supported`. The implicit flow is deprecated by OAuth 2.0
  Security BCP (RFC 9700) and removed from OAuth 2.1. Both lists now advertise
  only the flows this server actually implements: `authorization_code` and
  `refresh_token` grants, and `code` as the sole response type.

- **MED-5 fixed — Session copy of authorization code never cleaned up**
  (`Controller::Root`). The authorize endpoint wrote a copy of each issued code
  and its associated claims/scope/nonce into `$c->session->{oidc_code}`. This
  entry was never removed, causing stale PII to accumulate in the session store
  indefinitely. `_handle_authorization_code_grant` now calls
  `delete $c->session->{oidc_code}->{$code}` immediately after the code is
  successfully consumed.

### Tests

- **`t/01_jwt.t`** (4 new tests, 24 total) — MED-2: capturing logger verifies
  the `sign_token` debug message does not contain email or name fields, and does
  contain `sub` and `aud`.
- **`t/03_plugin.t`** (7 new tests) — MED-3: two distinct "application class"
  objects verified to hold isolated JWT instances; MED-4: discovery document
  verified to not contain `implicit` in grant types or implicit response types,
  and to still contain `authorization_code` / `code`.

---

## [0.05] - 2026-04-29 (Security Fixes: HIGH-1 through HIGH-5)

### Security

- **HIGH-1 fixed — Open Redirect in example login action** (`example/app.pl`).
  The `back` parameter was forwarded to `$c->response->redirect` without
  validation, allowing an attacker to craft a login URL that redirected the
  victim to an arbitrary external site after authentication. The parameter is
  now validated with `m{^/[^/]}` (must start with `/` followed by a
  non-`/` character) to reject both absolute URLs and protocol-relative `//`
  paths, and the redirect is issued via `$c->uri_for($back)`.

- **HIGH-2 fixed — Missing mandatory JWT claim validation** (`Utils::JWT`).
  `verify_token` previously only checked `exp` and `iss` when those claims were
  present. Both are now mandatory: tokens missing `exp` or `iss` are rejected;
  an expired `exp` is always rejected; an `iss` that does not match the
  configured issuer URL is rejected; `nbf` (not-before), when present, is
  enforced. An optional `expected_audience` parameter was also added: when
  supplied, the `aud` claim must be present and must match.

- **HIGH-3 fixed — Timing-vulnerable client secret comparison** (`Controller::Root`).
  The `eq` operator was used to compare client secrets at the token endpoint,
  leaking secret length and prefix information through timing side-channels.
  Both the authorization-code grant and the refresh-token grant now use
  `Crypt::Misc::slow_eq()` for constant-time comparison. `Crypt::Misc` added to
  `cpanfile`.

- **HIGH-4 fixed — TOCTOU race in authorization code redemption** (`Utils::Store`,
  `Utils::Store::Redis`, `Controller::Root`). The previous implementation called
  `get_authorization_code` followed by a separate `consume_authorization_code`,
  creating a window where two concurrent requests could both read the same code
  before either deleted it. `consume_authorization_code` is now a single atomic
  operation that fetches and deletes in one step (Perl `delete` for the
  in-memory store; Redis `GETDEL` (≥ 6.2) for the Redis store) and returns the
  code data hashref. The controller now calls only `consume_authorization_code`;
  the two-step pattern has been removed. `Role::Store` updated accordingly.

- **HIGH-5 fixed — No PKCE support** (`Controller::Root`, `Utils::Store`,
  `Utils::Store::Redis`, `Role::Store`). Full RFC 7636 PKCE implementation added:

  - **Authorize endpoint**: reads `code_challenge` and `code_challenge_method`
    from request parameters; persists them in the session so they survive the
    login redirect; enforces that public clients (those without a `client_secret`)
    **must** supply `code_challenge`; rejects any method other than `S256`
    (`plain` is not supported per OAuth 2.1 / security BCP); stores the
    challenge with the authorization code in both store backends.
  - **Token endpoint**: reads `code_verifier` from the POST body; after atomically
    consuming the code, verifies the challenge with
    `BASE64URL(SHA256(ASCII(code_verifier))) == code_challenge` using a
    constant-time comparison (`Crypt::Misc::slow_eq`); returns `invalid_grant`
    on failure.
  - **`_verify_pkce($verifier, $challenge)`** — private helper enforces verifier
    format (43–128 unreserved URI characters: `A-Z`, `a-z`, `0-9`, `-`, `.`,
    `_`, `~`) before computing and comparing the S256 challenge.
  - Both `Utils::Store` and `Utils::Store::Redis` accept an optional `$pkce`
    hashref in `create_authorization_code` and persist `code_challenge` /
    `code_challenge_method` with the code entry.

### Tests

- **`t/01_jwt.t`** (10 new tests, 20 total) — tests for `verify_token` mandatory
  claim enforcement: missing `exp`, expired token, missing `iss`, wrong issuer,
  future `nbf`, past `nbf`, `expected_audience` match, wrong audience, missing
  `aud` with `expected_audience`, missing `aud` without `expected_audience`.

- **`t/02_store.t`** (updated) — `consume_authorization_code` verified to return
  code data; second consume verified to return `undef`. Added PKCE round-trip
  tests: `code_challenge`/`code_challenge_method` stored and returned; no-PKCE
  case verified to leave those fields absent.

- **`t/04_store_redis.t`** (updated) — `MockRedis` gained a `getdel` method;
  tests confirm `GETDEL` is used (not `del`), consume returns data, second
  consume returns `undef`. Added PKCE round-trip tests through JSON
  serialization.

- **`t/06_pkce.t`** (new, 11 tests) — unit tests for `_verify_pkce`: correct
  verifier/challenge pair accepted; wrong verifier rejected; verifier too short
  (< 43) rejected; verifier too long (> 128) rejected; verifier with disallowed
  characters rejected; `undef` verifier rejected; `undef` challenge rejected;
  minimum (43-char) and maximum (128-char) length cases accepted; all unreserved
  char types accepted; tampered challenge rejected.

### Documentation

- **`API_REFERENCE.md`** — Authorization endpoint parameter table updated with
  `code_challenge` (Conditional) and `code_challenge_method` rows; token
  endpoint authorization-code grant table updated with `code_verifier`
  (Conditional) row and `client_secret` changed from Required to Conditional.
  New "PKCE-Protected Authorization Code Flow" example section added.
- **`IMPLEMENTATION_GUIDE.md`** — Authorization Code Flow steps updated with
  PKCE parameters; State Store module docs updated with accurate signatures and
  atomic-operation note; login action example updated with safe `back`
  validation; new PKCE subsection added under Security Considerations.
- **`QUICKSTART.md`** — Login action example updated with validated `back`
  redirect pattern.

---

## [0.04] - 2026-04-29 (Security Fix: Open Redirect in Logout Endpoint)

### Security

- **CRIT-1 fixed — Open Redirect in logout endpoint** (`Controller::Root`,
  `Utils::JWT`). The `post_logout_redirect_uri` parameter was previously
  forwarded without any validation, allowing an attacker to redirect victims to
  an arbitrary external URL after logout (phishing / credential harvesting).

  The logout flow now enforces the following rules, in line with OpenID Connect
  RP-Initiated Logout 1.0:

  1. `post_logout_redirect_uri` is rejected with `invalid_request` unless
     `id_token_hint` is also supplied.
  2. The hint token's RSA signature is verified to confirm it was genuinely
     issued by this server. Expiry is intentionally **not** checked — hint
     tokens are frequently expired at logout time by design.
  3. The `aud` claim of the verified hint identifies the requesting client.
     The `post_logout_redirect_uri` is then compared by **exact string match**
     against that client's registered `post_logout_redirect_uris` list.
     Prefix matching and host-only matching are not permitted.
  4. Any mismatch returns an `invalid_request` OAuth error; no redirect is
     issued.
  5. When a redirect is permitted, the optional `state` parameter is appended
     verbatim to the redirect URI as required by the specification.

### Added

- **`JWT::decode_id_token_hint($token)`** — new method on
  `Catalyst::Plugin::OpenIDConnect::Utils::JWT`. Verifies the token signature
  against the configured public key and returns the decoded claims hashref, or
  `undef` if the token is malformed or the signature is invalid. Distinct from
  `verify_token` in that it does not reject expired tokens.

- **`Controller::Root::_allowed_post_logout_uris($client)`** — private helper
  that normalises the `post_logout_redirect_uris` client config field from
  either an arrayref (YAML/JSON config) or a whitespace-delimited string
  (Config::General-style config) into a flat list of URIs.

- **`post_logout_redirect_uris` client config key** — each client may now
  declare a list of permitted post-logout redirect URIs. This key is required
  for clients that use `post_logout_redirect_uri` at the logout endpoint.

### Tests

- **`t/05_logout.t`** (new, 19 tests) — covers `decode_id_token_hint` for valid
  tokens, expired tokens, tampered tokens, wrong-key tokens, and structurally
  invalid JWTs; and `_allowed_post_logout_uris` for arrayref config, string
  config, missing config, and exact-match security semantics (prefix-of-registered
  and extended-path attacks).

### Documentation

- **`API_REFERENCE.md`** — Logout endpoint section rewritten with updated
  parameter table (marking `id_token_hint` as conditionally required), security
  note on exact-match validation, split request/response examples, full error
  response examples, and a client registration code snippet.
- **`README.md`** — Client configuration reference updated with the new
  `post_logout_redirect_uris` field.
- **`IMPLEMENTATION_GUIDE.md`** — Client configuration example and field list
  updated with `post_logout_redirect_uris`.
- **`DEPLOYMENT.md`** — Production `catalyst.conf` example updated with
  `post_logout_redirect_uris`.
- **`QUICKSTART.md`** — Quick-start Perl config example updated with
  `post_logout_redirect_uris`.
- **`example/app.pl`** — Both example clients now include
  `post_logout_redirect_uris`.

---

## [0.03] - 2026-04-24 (FastCGI / Multi-Process Store Support)

### Added

- **Catalyst::Plugin::OpenIDConnect::Role::Store** - New Moose role defining the
  pluggable store interface. Any store backend must `with` this role and implement
  three methods: `create_authorization_code`, `get_authorization_code`,
  `consume_authorization_code`. This decouples the plugin from a specific
  backend implementation.

- **Catalyst::Plugin::OpenIDConnect::Utils::Store::Redis** - Redis-backed store
  implementation for multi-process deployments (FastCGI, pre-forking servers).
  - Stores authorization codes in Redis with native TTL expiry via `SETEX`
  - Lazy Redis connection (opened after `fork()` so each worker has its own socket)
  - Supports `Redis::Fast` (preferred) or `Redis` client, auto-detected at runtime
  - Configurable key prefix for namespace isolation on shared Redis instances
  - Configurable code TTL (default 600 s)
  - Optional Redis `AUTH` password support
  - Blessed user objects serialised via `convert_blessed` JSON encoding

- **Configurable store class in plugin setup** - `Plugin::OpenIDConnect` config
  now accepts `store_class` and `store_args` keys, allowing any Role::Store
  consumer to be used as the backend without touching application code.

- **`Module::Runtime::require_module`** used for dynamic store class loading,
  replacing manual `s{::}{/}g` path mangling. Works correctly regardless of
  `@INC` ordering or non-filesystem module sources.

- **`Bytes::Random::Secure`** used for authorization code generation in both the
  memory and Redis store backends, replacing the previous `rand`-based generator.
  Codes are now drawn from the OS CSPRNG (`/dev/urandom`) and are safe to
  generate after `fork()`.

### Changed

- **`Catalyst::Plugin::OpenIDConnect::Utils::Store`** now consumes
  `Role::Store`, documents its multi-process limitation, and uses
  `Bytes::Random::Secure` for code generation.

- **`_oidc_store` accessor** now validates via `DOES('Role::Store')` instead of
  `isa('Utils::Store')`, accepting any conforming backend.

- **`Catalyst::Plugin::OpenIDConnect::Context::store()`** lazy initialisation
  now respects `store_class`/`store_args` config rather than always instantiating
  the in-memory store.

- **`Module::Runtime`** added as a declared dependency in `cpanfile`.
  `Bytes::Random::Secure` added as a core dependency. `Redis::Fast`/`Redis`
  listed as optional recommended dependencies under the `redis` feature.

### Tests

- **`t/02_store.t`** extended with: Role::Store compliance check, expiry
  enforcement, double-consume safety, CSPRNG uniqueness across 20 codes,
  `created_at`/`expires_at` field assertions, and missing-code undef check.

- **`t/04_store_redis.t`** (new) — 46 tests for the Redis store using an
  in-process `MockRedis` stub (no live Redis required). Covers: role compliance,
  create/get/consume lifecycle, `setex`/`del` call verification, key prefix and
  TTL configuration, code uniqueness, and corrupt-JSON graceful handling.

### Documentation

- **`DEPLOYMENT.md`** updated with a new "Redis Store (FastCGI and Multi-Process
  Deployments)" section covering installation, `catalyst.conf` and Perl hash
  config examples, production Redis hardening checklist (auth, TLS, memory
  policy, AOF persistence, namespacing), fork-safety explanation, custom backend
  table, updated Docker Compose example with a `redis:7-alpine` service, and
  three new troubleshooting entries.

---

## [0.02] - 2026-04-16 (Bug Fixes & Integration Improvements)

### Changed

- **Controller Integration**: Plugin now requires applications to create an extending controller in the app's namespace for proper route discovery. This ensures compatibility with Catalyst::Plugin::ACL and other route-processing plugins.
  - The plugin's controller (`Catalyst::Plugin::OpenIDConnect::Controller::Root`) is now a base class
  - Applications must create `lib/MyApp/Controller/OpenIDConnect.pm` that extends the plugin controller
  - This allows Catalyst to properly auto-discover routes and prevents dispatcher conflicts

- **Plugin Namespace Configuration**: Moved namespace configuration from extending controller to the base plugin controller
  - Base controller now sets `namespace => 'openidconnect'` by default
  - Extending controllers automatically inherit this configuration
  - Simplifies application setup

- **Simplified Plugin Lifecycle**: Changed from `setup_component`/`finalize_setup` to `after 'setup'` method modifier
  - Uses proper Moose role syntax for plugin hooks
  - Ensures correct execution order with other plugins like ACL

### Fixed

- Fixed "traversal hit a dead end" error when using plugin with existing apps that have route-processing plugins (ACL, etc.)
- Fixed plugin initialization to gracefully handle missing configuration
- Improved error handling for missing private key configuration

### Documentation

- Updated QUICKSTART.md with controller setup instructions
- Updated README.md with extending controller example
- Updated IMPLEMENTATION_GUIDE.md with detailed integration steps
- Updated DEPLOYMENT.md with production controller setup

---

## [0.01] - 2026-04-10 (Initial Release)

### Added

#### Core Implementation
- **Catalyst::Plugin::OpenIDConnect** - Main plugin module
  - Moose role for seamless Catalyst integration
  - Configuration management via catalyst.conf
  - Automatic JWT handler initialization
  - State store management
  - OIDC context object for controllers

- **Catalyst::Plugin::OpenIDConnect::Utils::JWT** - JWT utilities
  - RS256 (RSA SHA-256) signing algorithm
  - Token verification with signature validation
  - Support for ID tokens, access tokens, refresh tokens
  - URL-safe Base64 encoding (RFC 4648)
  - Standard claims handling (iss, aud, exp, iat, sub)
  - Debug decoding without verification

- **Catalyst::Plugin::OpenIDConnect::Utils::Store** - State management
  - In-memory authorization code storage
  - User session management
  - UUID-based session IDs
  - Automatic expiration handling
  - Code consumption (one-time use)
  - Cleanup utilities for expired entries

- **Catalyst::Plugin::OpenIDConnect::Controller::Root** - Protocol endpoints
  - Authorization endpoint (GET /openidconnect/authorize)
  - Token endpoint (POST /openidconnect/token)
  - UserInfo endpoint (GET /openidconnect/userinfo)
  - Discovery endpoint (GET /.well-known/openid-configuration)
  - JWKS endpoint (GET /openidconnect/jwks)
  - Logout endpoint (POST /openidconnect/logout)

#### OAuth 2.0 & OpenID Connect Features
- Authorization Code Flow (full implementation)
- Token Exchange
  - authorization_code grant type
  - refresh_token grant type
- State parameter (CSRF protection)
- Nonce binding
- PKCE-ready (for future implementation)
- Standard claims support
  - Profile claims (name, email, picture, etc.)
  - Email verification
  - Phone verification
  - Address claims
- Token types
  - ID tokens (with user claims)
  - Access tokens (for API access)
  - Refresh tokens (for token refresh)

#### Configuration
- YAML-based configuration via catalyst.conf
- Issuer configuration
  - URL for iss claim
  - RSA private/public key loading
  - Key ID for JWT headers
- Client configuration
  - client_id and client_secret
  - redirect_uris (multiple allowed)
  - response_types and grant_types
  - Scope declarations
- User claims mapping
  - Flexible attribute mapping to OIDC claims
  - Nested attribute support via dot notation
  - Optional claim definitions

#### Security Features
- HTTPS support (via reverse proxy)
- CSRF protection (state parameter)
- Authorization code expiration (10 minutes)
- One-time code consumption
- Session management with expiration
- Bearer token authentication
- JWT signature verification
- Client secret validation
- Redirect URI validation

#### Documentation
- **README.md** - Feature overview and quick start
- **QUICKSTART.md** - 5-minute getting started guide
- **IMPLEMENTATION_GUIDE.md** - Architecture and design decisions
- **API_REFERENCE.md** - Complete endpoint documentation
- **DEPLOYMENT.md** - Production deployment guide
- Inline POD documentation in all modules

#### Tests
- JWT functionality tests (01_jwt.t)
  - Token signing validation
  - Token verification validation
  - Token decoding
  - Invalid token rejection
  - Payload matching
- Store functionality tests (02_store.t)
  - Authorization code creation
  - Code retrieval and validation
  - Code consumption
  - Session management

#### Example Application
- **example/app.pl** - Working Catalyst application
  - Login page (demo login without password)
  - Protected resource example
  - Logout functionality
  - User session management
  - Three configured example clients
- **example/generate_keys.sh** - RSA key generation script
- **example/root/** - HTML templates
  - index.html (home page)
  - login.html (login form)
  - protected.html (protected resource)

#### Project Files
- **cpanfile** - Comprehensive dependency declarations
  - Catalyst and related modules
  - Cryptography libraries
  - JSON processing
  - Testing dependencies
- **dist.ini** - Distribution configuration for CPAN publishing
- Project structure ready for publication

### Implementation Details

#### Algorithm Support
- RS256 (RSA SHA-256) for all JWT operations
- 2048-bit RSA keys (4096-bit recommended for production)

#### Token Lifetimes
- Authorization codes: 10 minutes
- ID tokens: 1 hour
- Access tokens: 1 hour
- Refresh tokens: 30 days
- Sessions: 24 hours (configurable)

#### Standard Claims
- Supported: sub, name, given_name, family_name, email, picture, phone_number, etc.
- User-configurable mapping from application models
- Optional claims support

#### Endpoints
- All endpoints return JSON except authorization (redirects)
- Proper HTTP status codes (200, 302, 400, 401, 500)
- RFC 6749 & RFC 6750 compliance
- OpenID Connect 1.0 Core compliance

### Known Limitations

- In-memory state store (database integration requires extension)
- Single key at a time (key rotation requires restart)
- No HS256 support (RS256 only)
- No Implicit or Hybrid flows
- No PKCE (for public clients)
- No form_post response mode
- No client registration endpoint
- No introspection endpoint

### Requirements

- Perl 5.20 or higher
- Catalyst 5.90100 or higher
- Moose and related modules
- Crypt::OpenSSL modules
- JSON::MaybeXS
- HTTP::Request and LWP stack

### Testing

All modules have unit test coverage. Run tests with:

```bash
prove -l t/
```

### Future Roadmap

- [ ] PKCE support for public clients
- [ ] Implicit and Hybrid flow support
- [ ] Multiple simultaneous keys
- [ ] Database-backed session store
- [ ] Introspection endpoint
- [ ] Revocation endpoint
- [ ] Client metadata endpoint
- [ ] HS256 algorithm support
- [ ] Multi-signature support
- [ ] Request object support
- [ ] Pushed Authorization Requests (PAR)
- [ ] OpenID Connect Federation support

### Author

Tim F. Rayner

### License

This library is available under The Artistic License 2.0 (GPL Compatible). See LICENSE file for details.