Catalyst-Plugin-OpenIDConnect
view release on metacpan or search on metacpan
lib/Catalyst/Plugin/OpenIDConnect/Controller/Root.pm view on Meta::CPAN
sub logout : Local {
my ( $self, $c ) = @_;
my $config = $c->openidconnect->config;
$c->log->debug('Logout endpoint accessed') if $config->{debug};
my $redirect_uri = $c->request->params->{post_logout_redirect_uri};
my $id_token_hint = $c->request->params->{id_token_hint};
my $state = $c->request->params->{state};
# Decode the hint early â before the session is destroyed â so that we
# have the subject identifier available for refresh token revocation (MED-1).
my $hint_claims;
if ($id_token_hint) {
$hint_claims = $c->openidconnect->jwt->decode_id_token_hint($id_token_hint);
}
# Determine the subject identifier for refresh token revocation. Prefer
# the hint (authoritative); fall back to the live session so that
# logout-without-hint still revokes tokens when the session is present.
my $logout_sub = do {
if ( $hint_claims && $hint_claims->{sub} ) {
$hint_claims->{sub};
}
else {
my $sess_user = eval { $c->session->{user} };
$sess_user ? ( $sess_user->{sub} || $sess_user->{id} ) : undef;
}
};
# Clear user session
if ( $c->user ) {
$c->log->info('Logging out user: ' . $c->user->id);
$c->user->logout();
}
# Destroy session
if ( $c->sessionid ) {
$c->log->debug('Destroying session: ' . $c->sessionid) if $config->{debug};
$c->delete_session('User session destroyed');
}
# Revoke all outstanding refresh tokens for this user (MED-1).
if ($logout_sub) {
$c->openidconnect->store->revoke_refresh_tokens_for_user($logout_sub);
$c->log->debug("Refresh tokens revoked for user: $logout_sub")
if $config->{debug};
}
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) {
$c->log->warn('id_token_hint is missing the aud claim');
return $self->_json_error( $c, 'invalid_request',
'id_token_hint does not contain an aud claim' );
}
# Look up the client and validate the redirect URI against its
# registered post_logout_redirect_uris list.
my $client = $c->openidconnect->get_client($client_id);
unless ($client) {
$c->log->warn("Unknown client in id_token_hint aud claim: $client_id");
return $self->_json_error( $c, 'invalid_request',
'Unknown client in id_token_hint' );
}
my @allowed = _normalize_uri_list( $client->{post_logout_redirect_uris} );
unless ( grep { $_ eq $redirect_uri } @allowed ) {
$c->log->warn(
"Unregistered post_logout_redirect_uri for client $client_id: $redirect_uri"
);
return $self->_json_error( $c, 'invalid_request',
'post_logout_redirect_uri is not registered for this client' );
}
# Build the final redirect URI, appending state if supplied.
my $final_uri = URI->new($redirect_uri);
$final_uri->query_form( $final_uri->query_form, state => $state )
if defined $state && $state ne '';
$c->log->debug( 'Redirecting to post-logout URI: ' . $final_uri->as_string )
if $config->{debug};
return $c->response->redirect( $final_uri->as_string );
}
# Return success JSON response
$c->log->info('Logout completed successfully');
$self->_json_response( $c, {
message => 'Logged out successfully',
});
}
# Verify a PKCE code_verifier against a stored code_challenge.
# Only S256 (SHA-256) is supported; 'plain' is intentionally rejected.
# Returns a true value on success, false on failure.
sub _verify_pkce {
my ( $code_verifier, $code_challenge ) = @_;
return 0 unless defined $code_verifier && defined $code_challenge;
# Verifier must contain only unreserved URI chars and be 43-128 chars (RFC 7636 §4.1)
return 0 unless $code_verifier =~ /\A[A-Za-z0-9\-._~]{43,128}\z/;
# S256: BASE64URL( SHA256( ASCII( code_verifier ) ) )
my $computed = encode_base64url( sha256($code_verifier) );
( run in 0.528 second using v1.01-cache-2.11-cpan-6aa56a78535 )