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