Catalyst-Plugin-OpenIDConnect

 view release on metacpan or  search on metacpan

CHANGELOG.md  view on Meta::CPAN

     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

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

    if ($redirect_uri) {
        # id_token_hint is required when a redirect is requested so that we
        # can identify the client and verify the URI is registered for it.
        # Without this check an attacker could redirect to any arbitrary URL.
        unless ($id_token_hint) {
            $c->log->warn('post_logout_redirect_uri provided without id_token_hint');
            return $self->_json_error( $c, 'invalid_request',
                'id_token_hint is required when post_logout_redirect_uri is provided' );
        }

        # $hint_claims was decoded above; reject if invalid.
        unless ($hint_claims) {
            $c->log->warn('Invalid id_token_hint provided at logout');
            return $self->_json_error( $c, 'invalid_request', 'Invalid id_token_hint' );
        }

        # aud may be a string or an array per RFC 7519 §4.1.3.
        my $aud       = $hint_claims->{aud};
        my $client_id = ref $aud eq 'ARRAY' ? $aud->[0] : $aud;

        unless ($client_id) {

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

=head2 verify_token($token, %opts)

Verifies a JWT token with the configured public key.

Mandatory claims C<exp> and C<iss> are always validated.  The C<nbf>
claim is validated when present.  Pass C<expected_audience> to also
validate the C<aud> claim:

  $jwt->verify_token($token, expected_audience => 'my-client-id');

Returns a hashref with decoded claims on success.
Raises an exception on verification failure.

=cut

sub verify_token {
    my ( $self, $token, %opts ) = @_;
    my $expected_audience = $opts{expected_audience};

    $self->logger->debug('Verifying JWT token') if $self->logger;

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

            $signing_input,
            $signature
        );

        $self->logger->debug('JWT signature verified') if $self->logger;

        # Decode payload
        my $payload_json = _urlsafe_b64_decode($payload_b64);
        my $payload = decode_json($payload_json);

        $self->logger->debug('JWT payload decoded successfully') if $self->logger;

        # --- Mandatory claim validation (RFC 7519 §4.1, OIDC Core §2) ---
        # exp and iss must be present and valid; a token that omits them
        # must be rejected regardless of its signature.
        die 'Missing exp claim' unless defined $payload->{exp};
        die 'Token expired'     if $payload->{exp} < time();
        die 'Missing iss claim' unless defined $payload->{iss};
        die 'Invalid issuer'    unless $payload->{iss} eq $self->issuer;

        # nbf (not-before) is optional but must be honoured when present

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

    return $data;
}

=head2 consume_authorization_code($code)

Atomically fetches and deletes the authorization code from Redis using the
C<GETDEL> command (Redis E<ge> 6.2).  Because C<GETDEL> is a single server-side
operation it is race-free: a second concurrent request carrying the same code
will receive C<nil> from Redis and be rejected.

Returns the decoded code data hashref on success, or C<undef> if the code
does not exist, has already been consumed, or cannot be decoded.

=cut

sub consume_authorization_code {
    my ( $self, $code ) = @_;

    $self->logger->debug("Consuming authorization code: $code") if $self->logger;

    # GETDEL (Redis >= 6.2) fetches and deletes atomically in a single
    # round-trip, eliminating the GET + DEL race condition (HIGH-4).

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

    $self->_redis->setex( $self->prefix . 'rt:' . $jti, $ttl, $data );
    # Secondary index for bulk-revocation at logout.
    my $set_key = $self->prefix . 'rt_sub:' . $sub;
    $self->_redis->sadd( $set_key, $jti );
    $self->_redis->expire( $set_key, $ttl );
}

=head2 consume_refresh_token($jti)

Atomically fetches and deletes the JTI entry using C<GETDEL> (Redis E<ge> 6.2).
Returns the decoded data hashref, or C<undef> if absent (already used, revoked,
or expired).

=cut

sub consume_refresh_token {
    my ( $self, $jti ) = @_;
    my $raw = $self->_redis->getdel( $self->prefix . 'rt:' . $jti );
    return unless defined $raw;
    my $data = try { decode_json($raw) } catch { undef };
    if ($data) {



( run in 1.135 second using v1.01-cache-2.11-cpan-600a1bdf6e4 )