1package OpenXPKI::Client::UI; 2use Moose; 3 4use English; 5 6# Core modules 7use Encode; 8use Data::Dumper; 9use MIME::Base64; 10 11# CPAN modules 12use CGI::Session; 13use URI::Escape; 14use Log::Log4perl::MDC; 15use Crypt::JWT qw(encode_jwt); 16 17# Project modules 18use OpenXPKI::Template; 19use OpenXPKI::Client; 20use OpenXPKI::i18n qw( i18nGettext ); 21use OpenXPKI::Client::UI::Bootstrap; 22use OpenXPKI::Client::UI::Login; 23 24# ref to the cgi frontend session 25has 'session' => ( 26 required => 1, 27 is => 'rw', 28 isa => 'CGI::Session|Undef', 29); 30 31# the OXI::Client object 32has 'backend' => ( 33 is => 'rw', 34 isa => 'OpenXPKI::Client', 35 lazy => 1, 36 builder => '_init_backend', 37 trigger => \&_init_backend, 38); 39 40# should be passed by the ui script to be shared, if not we create it 41has 'logger' => ( 42 is => 'ro', 43 isa => 'Log::Log4perl::Logger', 44 lazy => 1, 45 default => sub{ return Log::Log4perl->get_logger( ); }, 46); 47 48has '_config' => ( 49 required => 1, 50 is => 'ro', 51 isa => 'HashRef', 52 init_arg => 'config', 53); 54 55# holds key object to sign socket communication 56has '_auth' => ( 57 is => 'ro', 58 isa => 'Ref', 59 init_arg => 'auth', 60 predicate => 'has_auth', 61); 62 63# Hold warnings from init 64has _status => ( 65 is => 'rw', 66 isa => 'HashRef|Undef', 67 lazy => 1, 68 default => undef 69); 70 71=head2 _init_backend 72 73Builder that creates an instance of OpenXPKI::Client and cares about 74switching/creating the backend session 75 76=cut 77sub _init_backend { 78 my $self = shift; 79 # the trigger has the client as argument, the builder has not 80 my $client = shift; 81 82 if (!$client) { 83 $client = OpenXPKI::Client->new({ 84 SOCKETFILE => $self->_config()->{'socket'}, 85 }); 86 $self->logger()->debug('Create backend client instance'); 87 } else { 88 $self->logger()->debug('Use provided client instance'); 89 } 90 91 my $client_id = $client->get_session_id(); 92 my $session = $self->session(); 93 my $backend_id = $session->param('backend_session_id') || undef; 94 95 if ($backend_id and $client_id and $backend_id eq $client_id) { 96 $self->logger()->debug('Backend session already loaded'); 97 } else { 98 eval { 99 $self->logger()->debug('First session reinit with id ' . ($backend_id || 'init')); 100 $client->init_session({ SESSION_ID => $backend_id }); 101 }; 102 if (my $eval_err = $EVAL_ERROR) { 103 my $exc = OpenXPKI::Exception->caught(); 104 if ($exc && $exc->message() eq 'I18N_OPENXPKI_CLIENT_INIT_SESSION_FAILED') { 105 $self->logger()->info('Backend session was gone - start a new one'); 106 # The session has gone - start a new one - might happen if the gui 107 # was idle too long or the server was flushed 108 $client->init_session({ SESSION_ID => undef }); 109 $self->_status({ level => 'warn', message => i18nGettext('I18N_OPENXPKI_UI_BACKEND_SESSION_GONE')}); 110 } else { 111 $self->logger()->error('Error creating backend session: ' . $eval_err->{message}); 112 $self->logger()->trace($eval_err); 113 die "Backend communication problem"; 114 } 115 } 116 # refresh variable to current id 117 $client_id = $client->get_session_id(); 118 } 119 120 # logging stuff only 121 if ($backend_id and $client_id eq $backend_id) { 122 $self->logger()->info('Resume backend session with id ' . $client_id); 123 } elsif ($backend_id) { 124 $self->logger()->info('Re-Init backend session ' . $client_id . '/' . $backend_id ); 125 } else { 126 $self->logger()->info('New backend session with id ' . $client_id); 127 } 128 $session->param('backend_session_id', $client_id); 129 130 Log::Log4perl::MDC->put('ssid', substr($client_id,0,4)); 131 132 $self->logger()->trace( Dumper $session->dataref ) if $self->logger->is_trace; 133 return $client; 134} 135 136 137sub BUILD { 138 my $self = shift; 139 140 if (!$self->session()->param('initialized')) { 141 my $session = $self->session(); 142 $session->param('initialized', 1); 143 $session->param('is_logged_in', 0); 144 $session->param('user', undef); 145 } elsif (my $user = $self->session()->param('user')) { 146 Log::Log4perl::MDC->put('name', $user->{name}); 147 Log::Log4perl::MDC->put('role', $user->{role}); 148 } else { 149 Log::Log4perl::MDC->put('name', undef); 150 Log::Log4perl::MDC->put('role', undef); 151 } 152 153} 154 155sub handle_request { 156 157 my $self = shift; 158 my $req = shift; 159 my $cgi = $req->cgi(); 160 161 my $page = $req->param('page') || ''; 162 my $action = $self->__get_action( $req ) || ''; 163 164 $self->logger()->debug('Incoming request: ' . join(', ', $page ? "page '$page'" : (), $action ? "action '$action'" : ())); 165 166 # Check for goto redirection first 167 if ($action =~ /^redirect!(.+)/ || $page =~ /^redirect!(.+)/) { 168 my $goto = $1; 169 my $result = OpenXPKI::Client::UI::Result->new({ client => $self, req => $req }); 170 $self->logger()->debug("Send redirect to $goto"); 171 $result->redirect( $goto ); 172 return $result->render(); 173 } 174 175 # Handle logout / session restart 176 # Do this before connecting the server to have the client in the 177 # new session and to recover from backend session failure 178 if ($page eq 'logout' || $action eq 'logout') { 179 180 # For SSO Logins the session might hold an external link 181 # to logout from the SSO provider 182 my $authinfo = $self->session()->param('authinfo') || {}; 183 my $redirectTo = $authinfo->{logout}; 184 185 # clear the session before redirecting to make sure we are safe 186 $self->logout_session( $cgi ); 187 $self->logger()->info('Logout from session'); 188 189 # now perform the redirect if set 190 if ($redirectTo) { 191 $self->logger()->debug("External redirect on logout to " . $redirectTo); 192 my $result = OpenXPKI::Client::UI::Result->new({ client => $self, req => $req }); 193 $result->redirect( $redirectTo ); 194 return $result->render(); 195 } 196 197 } 198 199 my $reply = $self->backend()->send_receive_service_msg('PING'); 200 my $status = $reply->{SERVICE_MSG}; 201 $self->logger()->trace('Ping replied ' . Dumper $reply) if $self->logger->is_trace; 202 $self->logger()->debug('current session status ' . $status); 203 204 if ( $reply->{SERVICE_MSG} eq 'START_SESSION' ) { 205 $reply = $self->backend()->init_session(); 206 $self->logger()->debug('Init new session'); 207 $self->logger()->trace('Init replied ' . Dumper $reply) if $self->logger->is_trace; 208 } 209 210 if ( $reply->{SERVICE_MSG} eq 'ERROR' ) { 211 my $result = OpenXPKI::Client::UI::Result->new({ client => $self, req => $req }); 212 $self->logger()->debug("Got error from server"); 213 return $result->set_status_from_error_reply( $reply ); 214 } 215 216 217 # Call to bootstrap components 218 if ($page =~ /^bootstrap!(.+)/) { 219 my $result = OpenXPKI::Client::UI::Bootstrap->new({ client => $self, req => $req }); 220 return $result->init_structure( )->render(); 221 } 222 223 # Only handle requests if we have an open channel 224 if ( $reply->{SERVICE_MSG} eq 'SERVICE_READY' ) { 225 return $self->handle_page( { req => $req } ); 226 } 227 228 # if the backend session logged out but did not terminate 229 # we get the problem that ui is logged in but backend is not 230 $self->logout_session( $cgi ) if ($self->session()->param('is_logged_in')); 231 232 # try to log in 233 return $self->handle_login( { req => $req, reply => $reply } ); 234 235} 236 237=head2 __load_class 238 239Expect the page/action string and a reference to the cgi object 240Extracts the expected class and method name and extra params encoded in 241the given parameter and tries to instantiate the class. On success, the 242class instance and the extracted method name is returned (two element 243array). On error, both elements in the array are set to undef. 244 245=cut 246 247sub __load_class { 248 249 my $self = shift; 250 my $call = shift; 251 my $req = shift; 252 253 $self->logger()->debug("Incoming call to load_class $call"); 254 255 my ($class, $method, $param) = ($call =~ /\A (\w+)\!? (\w+)? \!?(.*) \z/xms); 256 257 if (!$class) { 258 $self->logger()->error("Failed to parse page load string $call"); 259 return (undef, undef); 260 } 261 262 $method = 'index' if (!$method ); 263 264 my %extra; 265 if ($param) { 266 my @extra = split /!/, $param; 267 while (my $key = shift @extra) { 268 my $val = shift @extra // ''; 269 $extra{$key} = Encode::decode("UTF-8", uri_unescape($val)); 270 } 271 $self->logger()->trace("Found extra params " . Dumper \%extra ) if $self->logger->is_trace; 272 } 273 274 $self->logger()->debug("Loading handler class $class"); 275 276 $class = "OpenXPKI::Client::UI::".ucfirst($class); 277 eval "use $class;1"; 278 if ($EVAL_ERROR) { 279 $self->logger()->error("Failed loading handler class $class: $EVAL_ERROR"); 280 return (undef, undef); 281 } 282 283 my $result = $class->new({ client => $self, req => $req, extra => \%extra }); 284 285 return ($result, $method); 286 287} 288 289 290=head2 __get_action 291 292Expect a reference to the cgi object. Returns the value of 293cgi->param('action') if set and the XSRFtoken is valid. If the token is 294invalid, returns undef and sets the global status to error. If parameter 295is empty or not set returns undef. 296 297=cut 298 299sub __get_action { 300 301 my $self = shift; 302 my $req = shift; 303 304 my $rtoken_session = $self->session()->param('rtoken') || ''; 305 my $rtoken_request = $req->param('_rtoken') || ''; 306 # check XSRF token 307 if ($req->param('action')) { 308 if ($rtoken_request && ($rtoken_request eq $rtoken_session)) { 309 $self->logger()->debug("Valid action request - returning " . $req->param('action')); 310 return $req->param('action'); 311 312 # required to make the login page work when the session expires, #552 313 } elsif( !$rtoken_session and ($req->param('action') =~ /^login\!/ )) { 314 315 $self->logger()->debug("Login with expired session - ignoring rtoken"); 316 return $req->param('action'); 317 } else { 318 319 $self->logger()->debug("Request with invalid rtoken ($rtoken_request != $rtoken_session)!"); 320 $self->_status({ level => 'error', 'message' => i18nGettext('I18N_OPENXPKI_UI_REQUEST_TOKEN_NOT_VALID')}); 321 } 322 } 323 return; 324 325} 326 327 328sub __jwt_signature { 329 330 my $self = shift; 331 my $data = shift; 332 my $jws = shift; 333 334 return unless($self->has_auth()); 335 336 $self->logger()->debug('Sign data using key id ' . $jws->{keyid} ); 337 my $pkey = $self->_auth(); 338 return encode_jwt(payload => { 339 param => $data, 340 sid => $self->backend()->get_session_id(), 341 }, key=> $pkey, auto_iat => 1, alg=>'ES256'); 342 343} 344 345sub handle_page { 346 347 my $self = shift; 348 my $args = shift; 349 my $method_args = shift || {}; 350 351 my $req = $args->{req}; 352 my $cgi = $req->cgi(); 353 354 # set action and page - args always wins about cgi 355 356 my $result; 357 my $action = ''; 358 # action is only valid explicit or within a post request 359 if (defined $args->{action}) { 360 $action = $args->{action}; 361 } else { 362 $action = $self->__get_action( $req ); 363 } 364 365 $self->logger()->trace('Handle page: ' . Dumper { map { $_ => $args->{$_} } grep { $_ ne 'req' } keys %$args } ) if $self->logger->is_trace; 366 367 my $page = (defined $args->{page} ? $args->{page} : $req->param('page')) || 'home'; 368 369 if ($action) { 370 $self->logger()->info('handle action ' . $action); 371 372 my $method; 373 ($result, $method) = $self->__load_class( $action, $req ); 374 375 if ($result) { 376 $method = "action_$method"; 377 $self->logger()->debug("Method is $method"); 378 $result->$method( $method_args ); 379 } else { 380 $self->_status({ level => 'error', 'message' => i18nGettext('I18N_OPENXPKI_UI_ACTION_NOT_FOUND')}); 381 } 382 } 383 384 # Render a page only if there is no action result 385 if (!$result) { 386 387 # Handling of special page requests - to be replaced by hash if it grows 388 if ($page eq 'welcome') { 389 $page = 'home!welcome'; 390 } 391 392 my $method; 393 if ($page) { 394 ($result, $method) = $self->__load_class( $page, $req ); 395 } 396 397 if (!$result) { 398 $self->logger()->error("Failed loading page class"); 399 $result = OpenXPKI::Client::UI::Bootstrap->new({ client => $self, cgi => $cgi }); 400 $result->init_error(); 401 $result->set_status(i18nGettext('I18N_OPENXPKI_UI_PAGE_NOT_FOUND'),'error'); 402 403 } else { 404 $method = "init_$method"; 405 $self->logger()->debug("Method is $method"); 406 $result->$method( $method_args ); 407 } 408 } 409 410 Log::Log4perl::MDC->put('wfid', undef); 411 412 return $result->render(); 413 414} 415 416sub handle_login { 417 418 my $self = shift; 419 my $args = shift; 420 421 my $req = $args->{req}; 422 my $cgi = $req->cgi(); 423 my $reply = $args->{reply}; 424 425 $reply = $self->backend()->send_receive_service_msg('PING') if (!$reply); 426 427 my $status = $reply->{SERVICE_MSG}; 428 429 my $result = OpenXPKI::Client::UI::Login->new({ client => $self, req => $req }); 430 431 # Login works in three steps realm -> auth stack -> credentials 432 433 my $session = $self->session(); 434 my $page = $req->param('page') || ''; 435 436 # this is the incoming logout action 437 if ($page eq 'logout') { 438 $result->redirect( { goto => 'login!logout' } ); 439 return $result->render(); 440 } 441 442 # this is the redirect to the "you have been logged out page" 443 if ($page eq 'login!logout') { 444 return $result->init_logout()->render(); 445 } 446 447 # action is only valid within a post request 448 my $action = $self->__get_action( $req ) || ''; 449 450 $self->logger()->info('not logged in - doing auth - page is '.$page.' - action is ' . $action); 451 452 # Special handling for pki_realm and stack params 453 if ($action eq 'login!realm' && $req->param('pki_realm')) { 454 $session->param('pki_realm', scalar $req->param('pki_realm')); 455 $session->param('auth_stack', undef); 456 $self->logger()->debug('set realm in session: ' . $req->param('pki_realm') ); 457 } 458 if($action eq 'login!stack' && $req->param('auth_stack')) { 459 $session->param('auth_stack', scalar $req->param('auth_stack')); 460 $self->logger()->debug('set auth_stack in session: ' . $req->param('auth_stack') ); 461 } 462 463 # ENV always overrides session, keep this after the above block to prevent 464 # people from hacking into the session parameters 465 if ($ENV{OPENXPKI_PKI_REALM}) { 466 $session->param('pki_realm', $ENV{OPENXPKI_PKI_REALM}); 467 } 468 if ($ENV{OPENXPKI_AUTH_STACK}) { 469 $session->param('auth_stack', $ENV{OPENXPKI_AUTH_STACK}); 470 } 471 472 my $pki_realm = $session->param('pki_realm') || ''; 473 my $auth_stack = $session->param('auth_stack') || ''; 474 475 # if this is an initial request, force redirect to the login page 476 # will do an external redirect in case loginurl is set in config 477 if ($action !~ /^login/ && $page !~ /^login/) { 478 # Requests to pages can be redirected after login, store page in session 479 if ($page && $page ne 'logout' && $page ne 'welcome') { 480 $self->logger()->debug("Store page request for later redirect " . $page); 481 $self->session()->param('redirect', $page); 482 } 483 484 # Link to an internal method using the class!method 485 if (my $loginpage = $self->_config()->{loginpage}) { 486 487 # internal call to handle_page 488 return $self->handle_page({ action => '', page => $loginpage, req => $req }); 489 490 } elsif (my $loginurl = $self->_config()->{loginurl}) { 491 492 $self->logger()->debug("Redirect to external login page " . $loginurl ); 493 $result->redirect( { goto => $loginurl, type => 'external' } ); 494 return $result->render(); 495 # Do a real exit to skip the error handling of the script body 496 exit; 497 498 } elsif ( $cgi->http('HTTP_X-OPENXPKI-Client') ) { 499 500 # Session is gone but we are still in the ember application 501 $result->redirect('login'); 502 503 } else { 504 505 # This is not an ember request so we need to redirect 506 # back to the ember page - try if the session has a baseurl 507 my $url = $self->session()->param('baseurl'); 508 # if not, get the path from the referer 509 if (!$url && ($ENV{HTTP_REFERER} =~ m{https?://[^/]+(/[\w/]*[\w])/?}i)) { 510 $url = $1; 511 $self->logger()->debug('Restore redirect from referer'); 512 } 513 $url .= '/#/openxpki/login'; 514 $self->logger()->debug('Redirect to login page: ' . $url); 515 $result->redirect($url); 516 } 517 } 518 519 if ( $status eq 'GET_PKI_REALM' ) { 520 if ($pki_realm) { 521 $reply = $self->backend()->send_receive_service_msg( 'GET_PKI_REALM', { PKI_REALM => $pki_realm, } ); 522 $status = $reply->{SERVICE_MSG}; 523 $self->logger()->debug("Selected realm $pki_realm, new status " . $status); 524 } else { 525 my $realms = $reply->{'PARAMS'}->{'PKI_REALMS'}; 526 my @realm_list = map { $_ = {'value' => $realms->{$_}->{NAME}, 'label' => i18nGettext($realms->{$_}->{DESCRIPTION})} } keys %{$realms}; 527 $self->logger()->trace("Offering realms: " . Dumper \@realm_list ) if $self->logger->is_trace; 528 return $result->init_realm_select( \@realm_list )->render(); 529 } 530 } 531 532 if ( $status eq 'GET_AUTHENTICATION_STACK' ) { 533 # Never auth with an internal stack! 534 if ( $auth_stack && $auth_stack !~ /^_/) { 535 $self->logger()->debug("Authentication stack: $auth_stack"); 536 $reply = $self->backend()->send_receive_service_msg( 'GET_AUTHENTICATION_STACK', { 537 AUTHENTICATION_STACK => $auth_stack 538 }); 539 $status = $reply->{SERVICE_MSG}; 540 } else { 541 my $stacks = $reply->{'PARAMS'}->{'AUTHENTICATION_STACKS'}; 542 543 # List stacks and hide those starting with an underscore 544 my @stack_list = map { 545 ($stacks->{$_}->{name} !~ /^_/) ? ($_ = { 546 'value' => $stacks->{$_}->{name}, 547 'label' => i18nGettext($stacks->{$_}->{label}), 548 'description' => $stacks->{$_}->{description} 549 }) : () 550 } keys %{$stacks}; 551 552 # Directly load stack if there is only one 553 if (scalar @stack_list == 1) { 554 $auth_stack = $stack_list[0]->{value}; 555 $session->param('auth_stack', $auth_stack); 556 $self->logger()->debug("Only one stack avail ($auth_stack) - autoselect"); 557 $reply = $self->backend()->send_receive_service_msg( 'GET_AUTHENTICATION_STACK', { 558 AUTHENTICATION_STACK => $auth_stack 559 } ); 560 $status = $reply->{SERVICE_MSG}; 561 } else { 562 $self->logger()->trace("Offering stacks: " . Dumper \@stack_list ) if $self->logger->is_trace; 563 return $result->init_auth_stack( \@stack_list )->render(); 564 } 565 } 566 } 567 568 $self->logger()->debug("Selected realm $pki_realm, new status " . $status); 569 $self->logger()->trace('Reply: ' . Dumper $reply) if $self->logger->is_trace; 570 571 # we have more than one login handler and leave it to the login 572 # class to render it right. 573 if ( $status =~ /GET_(.*)_LOGIN/ ) { 574 my $login_type = $1; 575 576 ## FIXME - need a good way to configure login handlers 577 $self->logger()->info('Requested login type ' . $login_type ); 578 my $auth = $reply->{PARAMS}; 579 my $jws = $reply->{SIGN}; 580 581 # SSO Login uses data from the ENV, so no need to render anything 582 if ( $login_type eq 'CLIENT' ) { 583 584 $self->logger()->trace('ENV is ' . Dumper \%ENV) if $self->logger->is_trace; 585 my $data; 586 if ($auth->{envkeys}) { 587 foreach my $key (keys %{$auth->{envkeys}}) { 588 my $envkey = $auth->{envkeys}->{$key}; 589 $self->logger()->debug("Try to load $key from $envkey"); 590 next unless defined ($ENV{$envkey}); 591 $data->{$key} = $ENV{$envkey}; 592 } 593 # legacy support 594 } elsif (my $user = $ENV{'OPENXPKI_USER'} || $ENV{'REMOTE_USER'} || '') { 595 $data->{username} = $user; 596 $data->{role} = $ENV{'OPENXPKI_GROUP'} if($ENV{'OPENXPKI_GROUP'}); 597 } 598 599 # at least some items were found so we send them to the backend 600 if ($data) { 601 $self->logger()->trace('Sending auth data ' . Dumper $data) if $self->logger->is_trace; 602 603 $data = $self->__jwt_signature($data, $jws) if ($jws); 604 605 $reply = $self->backend()->send_receive_service_msg( 'GET_CLIENT_LOGIN', $data ); 606 607 # as nothing was found we do not even try to login in and look for a redirect 608 } elsif (my $loginurl = $auth->{login}) { 609 610 # the login url might contain a backlink to the running instance 611 $loginurl = OpenXPKI::Template->new()->render( $loginurl, 612 { baseurl => $session->param('baseurl') } ); 613 614 $self->logger()->debug("No auth data in environment - redirect found $loginurl"); 615 $result->redirect( { goto => $loginurl, type => 'external' } ); 616 return $result->render(); 617 618 # bad luck - something seems to be really wrong 619 } else { 620 $self->logger()->error('No ENV data to perform SSO Login'); 621 $self->logout_session( $cgi ); 622 return $result->init_login_missing_data()->render(); 623 } 624 625 } elsif ( $login_type eq 'X509' ) { 626 my $user = $ENV{'SSL_CLIENT_S_DN_CN'} || $ENV{'SSL_CLIENT_S_DN'}; 627 my $cert = $ENV{'SSL_CLIENT_CERT'} || ''; 628 629 $self->logger()->trace('ENV is ' . Dumper \%ENV) if $self->logger->is_trace; 630 631 if ($cert) { 632 $self->logger()->info('Sending X509 Login ( '.$user.' )'); 633 my @chain; 634 # larger chains are very unlikely and we dont support stupid clients 635 for (my $cc=0;$cc<=3;$cc++) { 636 my $chaincert = $ENV{'SSL_CLIENT_CERT_CHAIN_'.$cc}; 637 last unless ($chaincert); 638 push @chain, $chaincert; 639 } 640 641 my $data = { certificate => $cert, chain => \@chain }; 642 $data = $self->__jwt_signature($data, $jws) if ($jws); 643 644 $reply = $self->backend()->send_receive_service_msg( 'GET_X509_LOGIN', $data); 645 $self->logger()->trace('Auth result ' . Dumper $reply) if $self->logger->is_trace; 646 } else { 647 $self->logger()->error('Certificate missing for X509 Login'); 648 $self->logout_session( $cgi ); 649 return $result->init_login_missing_data()->render(); 650 } 651 652 } elsif( $login_type eq 'PASSWD' ) { 653 654 # form send / credentials are passed (works with an empty form too...) 655 656 if (($self->__get_action($req) || '') eq 'login!password') { 657 $self->logger()->debug('Seems to be an auth try - validating'); 658 ##FIXME - Input validation 659 660 my $data; 661 my @fields = $auth->{field} ? 662 (map { $_->{name} } @{$auth->{field}}) : 663 ('username','password'); 664 665 foreach my $field (@fields) { 666 my $val = $req->param($field); 667 next unless ($val); 668 $data->{$field} = $val; 669 } 670 671 $data = $self->__jwt_signature($data, $jws) if ($jws); 672 673 $reply = $self->backend()->send_receive_service_msg( 'GET_PASSWD_LOGIN', $data ); 674 $self->logger()->trace('Auth result ' . Dumper $reply) if $self->logger->is_trace; 675 676 } else { 677 $self->logger()->debug('No credentials, render form'); 678 return $result->init_login_passwd($auth)->render(); 679 } 680 681 } else { 682 683 $self->logger()->warn('Unknown login type ' . $login_type ); 684 } 685 } 686 687 if ( $reply->{SERVICE_MSG} eq 'SERVICE_READY' ) { 688 $self->logger()->info('Authentication successul - fetch session info'); 689 # Fetch the user info from the server 690 $reply = $self->backend()->send_receive_service_msg( 'COMMAND', 691 { COMMAND => 'get_session_info', PARAMS => {}, API => 2 } ); 692 693 if ( $reply->{SERVICE_MSG} eq 'COMMAND' ) { 694 695 my $session_info = $reply->{PARAMS}; 696 697 # merge baseurl to authinfo links 698 # (we need to get the baseurl before recreating the session below) 699 my $auth_info = {}; 700 my $baseurl = $session->param('baseurl'); 701 if (my $ai = $session_info->{authinfo}) { 702 my $tt = OpenXPKI::Template->new; 703 for my $key (keys %{$ai}) { 704 $auth_info->{$key} = $tt->render( $ai->{$key}, { baseurl => $baseurl } ); 705 } 706 } 707 delete $session_info->{authinfo}; 708 709 #$self->backend()->rekey_session(); 710 #my $new_backend_session_id = $self->backend()->get_session_id(); 711 712 # Generate a new frontend session to prevent session fixation 713 # The backend session remains the same but can not be used by an 714 # adversary as the id is never exposed and we destroy the old frontend 715 # session so access to the old session is not possible 716 $self->_recreate_frontend_session($session, $session_info, $auth_info); 717 718 Log::Log4perl::MDC->put('sid', substr($session->id,0,4)); 719 720 # FIXME Remove direct access to $main::cookie and main::encrypt_cookie 721 if ($main::cookie) { 722 $main::cookie->{'-value'} = main::encrypt_cookie($session->id); 723 push @main::header, ('-cookie', $cgi->cookie( $main::cookie )); 724 } 725 $self->logger->trace('CGI Header ' . Dumper \@main::header ) if $self->logger->is_trace; 726 727 if ($auth_info->{login}) { 728 $result->redirect( $auth_info->{login} ); 729 } else { 730 $result->init_index(); 731 } 732 return $result->render(); 733 } 734 } 735 736 if ( $reply->{SERVICE_MSG} eq 'ERROR') { 737 738 $self->logger()->trace('Server Error Msg: '. Dumper $reply) if $self->logger->is_trace; 739 740 # Failure here is likely a wrong password 741 742 if ($reply->{'ERROR'} && $reply->{'ERROR'}->{CLASS} eq 'OpenXPKI::Exception::Authentication') { 743 $result->set_status(i18nGettext( $reply->{'ERROR'}->{LABEL} ),'error'); 744 } else { 745 $result->set_status_from_error_reply($reply); 746 } 747 return $result->render(); 748 } 749 750 $self->logger()->debug("unhandled error during auth"); 751 return; 752 753} 754 755sub _recreate_frontend_session() { 756 757 my $self = shift; 758 my $session = shift; 759 my $data = shift; 760 my $auth_info = shift; 761 762 $self->logger->trace('Got session info: '. Dumper $data) if $self->logger->is_trace; 763 764 # fetch redirect from old session before deleting it! 765 my $redirect = $session->param('redirect'); 766 767 # delete the old instance data 768 $session->delete; 769 $session->flush; 770 # call new on the existing session object to reuse settings 771 $session = $session->new; 772 773 $self->logger->debug('New frontend session id : '. $session->id ); 774 775 if ($redirect) { 776 $self->logger->trace('Carry over redirect target ' . $redirect); 777 $session->param('redirect', $redirect); 778 } 779 780 # set some data 781 $session->param('backend_session_id', $self->backend->get_session_id ); 782 783 # move userinfo to own node 784 $session->param('userinfo', $data->{userinfo} || {}); 785 delete $data->{userinfo}; 786 787 $session->param('authinfo', $auth_info); 788 789 $session->param('user', $data); 790 $session->param('pki_realm', $data->{pki_realm}); 791 $session->param('is_logged_in', 1); 792 $session->param('initialized', 1); 793 794 $self->session($session); 795 796 # Check for MOTD 797 my $motd = $self->backend->send_receive_command_msg( 'get_motd' ); 798 if (ref $motd->{PARAMS} eq 'HASH') { 799 $self->logger->trace('Got MOTD: '. Dumper $motd->{PARAMS} ) if $self->logger->is_trace; 800 $session->param('motd', $motd->{PARAMS} ); 801 } 802 803 # menu 804 my $reply = $self->backend->send_receive_command_msg( 'get_menu' ); 805 $self->_set_menu($session, $reply->{PARAMS}) if ref $reply->{PARAMS} eq 'HASH'; 806 807 $session->flush; 808 809} 810 811sub _set_menu { 812 my $self = shift; 813 my $session = shift; 814 my $menu = shift; 815 816 $self->logger->trace('Menu ' . Dumper $menu) if $self->logger->is_trace; 817 818 $session->param('menu', $menu->{main}); 819 820 # persist the optional parts of the menu hash (landmark, tasklist, search attribs) 821 $session->param('landmark', $menu->{landmark} || {}); 822 $self->logger->trace('Got landmarks: ' . Dumper $menu->{landmark}) if $self->logger->is_trace; 823 824 # Keepalive pings to endpoint 825 if ($menu->{ping}) { 826 my $ping; 827 if (ref $menu->{ping} eq 'HASH') { 828 $ping = $menu->{ping}; 829 $ping->{timeout} *= 1000; # Javascript expects timeout in ms 830 } else { 831 $ping = { href => $menu->{ping}, timeout => 120000 }; 832 } 833 $session->param('ping', $ping); 834 } 835 836 # tasklist, wfsearch, certsearch and bulk can have multiple branches 837 # using named keys. We try to autodetect legacy formats and map 838 # those to a "default" key 839 # TODO Remove legacy compatibility 840 841 # config items are a list of hashes 842 foreach my $key (qw(tasklist bulk)) { 843 844 if (ref $menu->{$key} eq 'ARRAY') { 845 $session->param($key, { 'default' => $menu->{$key} }); 846 } elsif (ref $menu->{$key} eq 'HASH') { 847 $session->param($key, $menu->{$key} ); 848 } else { 849 $session->param($key, { 'default' => [] }); 850 } 851 $self->logger->trace("Got $key: " . Dumper $menu->{$key}) if $self->logger->is_trace; 852 } 853 854 # top level is a hash that must have a "attributes" node 855 # legacy format was a single list of attributes 856 # TODO Remove legacy compatibility 857 foreach my $key (qw(wfsearch certsearch)) { 858 859 # plain attributes 860 if (ref $menu->{$key} eq 'ARRAY') { 861 $session->param($key, { 'default' => { attributes => $menu->{$key} } } ); 862 } elsif (ref $menu->{$key} eq 'HASH') { 863 $session->param($key, $menu->{$key} ); 864 } else { 865 $session->param($key, { 'default' => {} }); 866 } 867 $self->logger->trace("Got $key: " . Dumper $menu->{$key}) if $self->logger->is_trace; 868 } 869 870 # Check syntax of "certdetails". 871 # (the sub{} below allows using "return" instead of nested "if"-structures) 872 my $certdetails = sub { 873 my $result; 874 unless ($result = $menu->{certdetails}) { 875 $self->logger->warn('Config entry "certdetails" is empty'); 876 return {}; 877 } 878 unless (ref $result eq 'HASH') { 879 $self->logger->warn('Config entry "certdetails" is not a hash'); 880 return {}; 881 } 882 if ($result->{metadata}) { 883 if (ref $result->{metadata} eq 'ARRAY') { 884 for my $md (@{ $result->{metadata} }) { 885 if (not ref $md eq 'HASH') { 886 $self->logger->warn('Config entry "certdetails.metadata" contains an item that is not a hash'); 887 $result->{metadata} = []; 888 last; 889 } 890 } 891 } 892 else { 893 $self->logger->warn('Config entry "certdetails.metadata" is not an array'); 894 $result->{metadata} = []; 895 } 896 } 897 return $result; 898 }->(); 899 $session->param('certdetails', $certdetails); 900 901 # Check syntax of "wfdetails". 902 # (the sub{} below allows using "return" instead of nested "if"-structures) 903 my $wfdetails = sub { 904 if (not exists $menu->{wfdetails}) { 905 $self->logger->debug('Config entry "wfdetails" is not defined, using defaults'); 906 return []; 907 } 908 my $result; 909 unless ($result = $menu->{wfdetails}) { 910 $self->logger->debug('Config entry "wfdetails" is set to "undef", hide from output'); 911 return; 912 } 913 unless (ref $result eq 'ARRAY') { 914 $self->logger->warn('Config entry "wfdetails" is not an array'); 915 return []; 916 } 917 return $result; 918 }->(); 919 $session->param('wfdetails', $wfdetails); 920} 921 922=head2 logout_session 923 924Delete and flush the current session and recreate a new one using 925the remaining class object. If the internal session handler is used, 926the session is cleared but not destreoyed. 927 928If you pass a reference to the CGI handler, the session cookie is updated. 929=cut 930 931sub logout_session { 932 933 my $self = shift; 934 my $cgi = shift; 935 936 $self->logger->info("session logout"); 937 938 my $session = $self->session(); 939 $self->backend()->logout(); 940 $self->session()->delete(); 941 $self->session()->flush(); 942 $self->session( $self->session()->new() ); 943 944 Log::Log4perl::MDC->put('sid', substr($self->session->id,0,4)); 945 946 # flush the session cookie 947 if ($cgi && $main::cookie) { 948 $main::cookie->{'-value'} = main::encrypt_cookie($self->session->id); 949 push @main::header, ('-cookie', $cgi->cookie( $main::cookie )); 950 } 951 952} 953 954 9551; 956