#!/usr/bin/perl # # DW::Controller::OpenID # # This controller is for OpenID related pages. # # Authors: # Mark Smith # Jen Griffin # # Copyright (c) 2011-2020 by Dreamwidth Studios, LLC. # # This program is free software; you may redistribute it and/or modify it under # the same terms as Perl itself. For a copy of the license, please reference # 'perldoc perlartistic' or 'perldoc perlgpl'. # package DW::Controller::OpenID; use strict; use warnings; use DW::Routing; use DW::Controller; use DW::Template; use LJ::OpenID; DW::Routing->register_string( '/openid/index', \&openid_index_handler, app => 1 ); DW::Routing->register_string( '/openid/options', \&openid_options_handler, app => 1 ); # for responding to OpenID authentication requests DW::Routing->register_string( '/openid/server', \&openid_server_handler, app => 1, no_cache => 1 ); # for claiming imported comments DW::Routing->register_string( '/openid/claim', \&openid_claim_handler, app => 1 ); DW::Routing->register_string( '/openid/claimed', \&openid_claimed_handler, app => 1 ); DW::Routing->register_string( '/openid/claim_confirm', \&openid_claim_confirm_handler, app => 1 ); # for handling site logins DW::Routing->register_string( '/openid/login', \&openid_login_handler, app => 1, no_cache => 1 ); # form for approving an OpenID login elsewhere using credentials from our site DW::Routing->register_string( '/openid/approve', \&openid_approve_handler, app => 1 ); sub openid_index_handler { my ( $ok, $rv ) = controller( anonymous => 1 ); return $rv unless $ok; my $r = $rv->{r}; my $vars = { continue_to => $r->get_args->{returnto} || $r->header_in("Referer") }; return DW::Template->render_template( 'openid/index.tt', $vars ); } sub openid_options_handler { my ( $ok, $rv ) = controller(); return $rv unless $ok; my $r = $rv->{r}; my $u = $rv->{remote}; my $dbh = LJ::get_db_writer(); my $trusted = {}; my $load_trusted = sub { $trusted = $dbh->selectall_hashref( q{ SELECT ye.endpoint_id as 'endid', ye.url FROM openid_endpoint ye, openid_trust yt WHERE yt.endpoint_id=ye.endpoint_id AND yt.userid=? }, 'endid', undef, $u->userid ); }; $load_trusted->(); # check for deletions if ( $r->did_post ) { foreach my $endid ( keys %$trusted ) { next unless $r->post_args->{"delete:$endid"}; $dbh->do( "DELETE FROM openid_trust WHERE userid=? AND endpoint_id=?", undef, $u->userid, $endid ); } $load_trusted->(); } # construct row data my @rows; my $url_sort = sub { $trusted->{$a}->{url} cmp $trusted->{$b}->{url} }; foreach my $endid ( sort $url_sort keys %$trusted ) { push @rows, [ "delete:$endid", $trusted->{$endid}->{url} ]; } $rv->{rows} = \@rows; return DW::Template->render_template( 'openid/options.tt', $rv ); } sub openid_server_handler { my $r = DW::Request->get; my $get = $r->get_args; my $trust_root = $get->{'openid.trust_root'} // ''; my $return_to = $get->{'openid.return_to'} // ''; ## Non-OpenID-compliant section: rewrite LiveJournal's trust_root to ## https so that it will match their return_to URL and pass validation. $get->{'openid.trust_root'} = 'https://www.livejournal.com/' if ( $trust_root eq 'http://www.livejournal.com/' && $return_to =~ m|^https://www\.livejournal\.com/| ); my $nos = LJ::OpenID::server( $get, $r->post_args ); my ( $type, $data ) = $nos->handle_page( redirect_for_setup => 1 ); return $r->redirect($data) if $type eq "redirect"; $r->content_type($type) if $type; $r->print($data); return $r->OK; } sub openid_claim_handler { my $opts = shift; my $r = DW::Request->get; my ( $ok, $rv ) = controller(); return $rv unless $ok; my $err = sub { my @errors = map { $_ =~ /^\./ ? LJ::Lang::ml("/openid/claim.tt$_") : $_ } @_; return DW::Template->render_template( 'openid/claim.tt', { error_list => \@errors } ); }; my $u = $rv->{remote}; my @claims = $u->get_openid_claims; return DW::Template->render_template( 'openid/claim.tt', { claims => \@claims } ) unless $r->did_post; # at this point, the user did a POST, so we want to try to perform an OpenID # login on the given URL. my $args = $r->post_args; my $url = LJ::trim( $args->{openid_url} ); return $err->('.error.required') unless $url; return $err->('.error.invalidchars') if $url =~ /[\<\>\s]/; my $csr = LJ::OpenID::consumer(); my $tried_local_ref = LJ::OpenID::blocked_hosts($csr); my $claimed_id = eval { $csr->claimed_identity($url); }; return $err->($@) if $@; unless ($claimed_id) { return $err->( LJ::Lang::ml( '/openid/claim.tt.error.cantuseownsite', { sitename => $LJ::SITENAMESHORT } ) ) if $$tried_local_ref; return $err->( $csr->err ); } my $check_url = $claimed_id->check_url( return_to => "$LJ::SITEROOT/openid/claimed", trust_root => "$LJ::SITEROOT/", delayed_return => 1, ); return $r->redirect($check_url); } sub openid_claimed_handler { my $opts = shift; my $r = DW::Request->get; my ( $ok, $rv ) = controller(); return $rv unless $ok; my $err = sub { my @errors = map { $_ =~ /^\./ ? LJ::Lang::ml("/openid/claim.tt$_") : $_ } @_; return DW::Template->render_template( 'openid/claim.tt', { error_list => \@errors } ); }; # attempt to verify the user my $u = $rv->{remote}; my $args = $r->get_args; return $r->redirect("$LJ::SITEROOT/openid/claim") unless exists $args->{'openid.mode'}; my $csr = LJ::OpenID::consumer( $args->as_hashref ); return $r->redirect("$LJ::SITEROOT/openid/claim") if $csr->user_cancel; my $setup = $csr->user_setup_url; return $r->redirect($setup) if $setup; my $return_to = "$LJ::SITEROOT/openid/claimed"; return $r->redirect("$LJ::SITEROOT/openid/claim") if $args->{'openid.return_to'} && $args->{'openid.return_to'} !~ /^\Q$return_to\E/; my $vident = eval { $csr->verified_identity; }; return $err->($@) if $@; return $err->( $csr->err ) unless $vident; my $url = $vident->url; return $err->('.error.invalidchars') if $url =~ /[\<\>\s]/; my $ou = LJ::User::load_identity_user( 'O', $url, $vident ); return $err->('.error.failed_vivification') unless $ou; return $err->( LJ::Lang::ml( '/openid/claim.tt.error.account_deleted', { sitename => $LJ::SITENAMESHORT, aopts1 => '/openid', aopts2 => '/accountstatus', } ) ) if $ou->is_deleted; # generate the authaction my $aa = LJ::register_authaction( $u->id, 'claimopenid', $ou->id ) or return $err->('Internal error generating authaction.'); my $confirm_url = "$LJ::SITEROOT/openid/claim_confirm?auth=$aa->{aaid}.$aa->{authcode}"; # great, let's send them an email to confirm my $email = LJ::Lang::ml( '/openid/claim.tt.email', { sitename => $LJ::SITENAME, sitenameshort => $LJ::SITENAMESHORT, remote => $u->display_name, openid => $ou->display_name, confirm_url => $confirm_url, } ); LJ::send_mail( { to => $u->email_raw, from => $LJ::ADMIN_EMAIL, subject => LJ::Lang::ml( '/openid/claim.tt.email.subject', { sitename => $LJ::SITENAME } ), body => $email, } ); # now give them the conf page return DW::Template->render_template('openid/claim_sent.tt'); } sub openid_claim_confirm_handler { my $opts = shift; my $r = DW::Request->get; my ( $ok, $rv ) = controller(); return $rv unless $ok; my $err = sub { my @errors = map { $_ =~ /^\./ ? LJ::Lang::ml("/openid/claim_confirm.tt$_") : $_ } @_; return DW::Template->render_template( 'openid/claim_confirm.tt', { error_list => \@errors } ); }; my $u = $rv->{remote}; my @claims = $u->get_openid_claims; my $args = $r->get_args; # verify that the link they followed is good my ( $aaid, $authcode ); ( $aaid, $authcode ) = ( $1, $2 ) if $args->{auth} =~ /^(\d+)\.(\w+)$/; my $aa = LJ::is_valid_authaction( $aaid, $authcode ); return $err->('.error.invalid_auth') unless $aa && ref $aa eq 'HASH' && $aa->{used} eq 'N' && $aa->{action} eq 'claimopenid'; return $err->('.error.wrong_account') if $aa->{userid} != $u->id; # now make sure nobody has since claimed that account my $ou = LJ::load_userid( $aa->{arg1} + 0 ); return $err->('.error.invalid_account') unless $ou && $ou->is_identity; if ( my $cbu = $ou->claimed_by ) { return $err->('.error.already_claimed_self') if $cbu->equals($u); return $err->('.error.already_claimed_other'); } return $err->( LJ::Lang::ml( '/openid/claim.tt.error.account_deleted', { sitename => $LJ::SITENAMESHORT, aopts1 => '/openid', aopts2 => '/accountstatus', } ) ) if $ou->is_deleted; # now start the claim process $u->claim_identity($ou); return DW::Template->render_template('openid/claim_confirm.tt'); } sub openid_login_handler { my ( $ok, $rv ) = controller( anonymous => 1 ); return $rv unless $ok; my $remote = $rv->{remote}; return error_ml( '/openid/login.tt.error.logout.content', { user => $remote->ljuser_display } ) if $remote; my $r = $rv->{r}; # yes, this looks at post_args without checking for did_post. # that's how the old page operated so I'm preserving that behavior. my $continue_to = $r->post_args->{'continue_to'}; my $query_args = $continue_to ? "?continue_to=" . LJ::eurl($continue_to) : ""; my $return_to = "$LJ::SITEROOT/openid/login" . $query_args; if ( $r->did_post ) { my $csr = LJ::OpenID::consumer(); my $url = $r->post_args->{'openid_url'}; return error_ml('/openid/login.tt.error.invalidcharacters') if $url =~ /[\<\>\s]/; my $tried_local_ref = LJ::OpenID::blocked_hosts($csr); my $claimed_id = eval { $csr->claimed_identity($url); }; return error_ml( '/openid/login.tt.error.claimed_identity', { err => $@ } ) if $@; unless ($claimed_id) { return error_ml( '/openid/login.tt.error.cantuseownsite', { sitename => $LJ::SITENAMESHORT } ) if $$tried_local_ref; return error_ml( '/openid/login.tt.error.loading_identity', { err => $csr->err } ); } my $check_url = $claimed_id->check_url( return_to => $return_to, trust_root => "$LJ::SITEROOT/", delayed_return => 1, ); return $r->redirect($check_url); } # get_args method returns a Hash::MultiValue, # but LJ::OpenID::consumer needs a plain vanilla hash my $get_args = $r->get_args->as_hashref; if ( $get_args->{'openid.mode'} ) { my $csr = LJ::OpenID::consumer($get_args); return $r->redirect("$LJ::SITEROOT/openid/") if $csr->user_cancel; my $setup = $csr->user_setup_url; return $r->redirect($setup) if $setup; if ( $get_args->{'openid.return_to'} && $get_args->{'openid.return_to'} !~ /^\Q$return_to\E/ ) { return error_ml( '/openid/login.tt.error.invalidparameter', { item => "return_to" } ); } my $errmsg; my $u = LJ::User::load_from_consumer( $csr, \$errmsg ); return error_ml( '/openid/login.tt.error.consumer_object', { err => $errmsg } ) unless $u; my $sess_opts = { exptype => 'short', ipfixed => 0 }; my $etime = 0; # yes, we're looking at post args in a get request again. my $expire_arg = $r->post_args->{expire} // ''; if ( $expire_arg eq "never" ) { $etime = time() + 60 * 60 * 24 * 60; $sess_opts->{'exptype'} = "long"; } $u->make_login_session( $sess_opts->{'exptype'}, $sess_opts->{'ipfixed'} ); LJ::set_remote($u); my $redirect = "$LJ::SITEROOT/login"; # handle the continue_to url, if it's a valid URL to redirect to. $continue_to = $get_args->{'continue_to'}; if ($continue_to) { # some pages return a relative url if ( $continue_to =~ /^\// ) { $continue_to = $LJ::SITEROOT . $continue_to; } if ( DW::Controller::validate_redirect_url($continue_to) ) { # if the account is validated, then go ahead and redirect if ( $u->is_validated ) { $redirect = $continue_to; } else { $redirect .= "?continue_to=" . LJ::eurl($continue_to); } } } return $r->redirect($redirect); } # the old code displayed an empty page if we got this far. let's do better. return $r->redirect("$LJ::SITEROOT/login"); } sub openid_approve_handler { my ( $ok, $rv ) = controller( form_auth => 1 ); return $rv unless $ok; my $u = $rv->{remote}; return error_ml('/openid/approve.tt.error.login_needed') unless $u; my $r = $rv->{r}; my $args = $r->get_args; # redirect to the main OpenID page if we tried to access this directly return $r->redirect("$LJ::SITEROOT/openid/") unless $args->{'identity'}; my $identity = LJ::OpenID::is_identity( $u, $args->{'identity'}, $args ); unless ($identity) { return error_ml( '/openid/approve.tt.error.cannot_provide_identity', { url => LJ::ehtml( $args->{'identity'} ), user => $u->ljuser_display } ); } my $site = $args->{'trust_root'}; $site =~ s/\?.*//; return error_ml('/openid/approve.tt.error.invalid_site_address') unless $site =~ m!^https?://!; # TODO from original bml file: check URL and see if it contains images or # external scripts/css/images, where an attacker could sniff the validation # tokens in the Referer header? my $nos = LJ::OpenID::server(); my $sig_return = $nos->signed_return_url( identity => $args->{'identity'}, return_to => $args->{'return_to'}, trust_root => $args->{'trust_root'}, assoc_handle => $args->{'assoc_handle'}, ); ## ## If user is logged in, and user trusts the site, then ## we can tell the user's identity to the site. ## if ( LJ::OpenID::is_trusted( $u, $site ) ) { return $r->redirect($sig_return) if $sig_return; } if ( $r->did_post ) { # form_auth is checked in the controller function my $post = $r->post_args; if ( $post->{'no'} ) { my $cancel_url = $nos->cancel_return_url( return_to => $args->{'return_to'}, ); return $r->redirect($cancel_url); } if ( $post->{'yes:always'} ) { LJ::OpenID::add_trust( $u, $site ) or return error_ml('/openid/approve.tt.error.failed_to_save'); } return $r->redirect($sig_return) if $sig_return; return error_ml('/openid/approve.tt.error.failed_sign_url'); } my $disp_site = LJ::ehtml($site); $disp_site =~ s!\*\.!<anything>.!; my $vars = { disp_idurl => LJ::ehtml( $args->{'identity'} ), disp_site => $disp_site, }; return DW::Template->render_template( 'openid/approve.tt', $vars ); } 1;