mourningdove/cgi-bin/DW/Controller/OpenID.pm
2026-05-24 01:03:05 +00:00

492 lines
16 KiB
Perl

#!/usr/bin/perl
#
# DW::Controller::OpenID
#
# This controller is for OpenID related pages.
#
# Authors:
# Mark Smith <mark@dreamwidth.org>
# Jen Griffin <kareila@livejournal.com>
#
# 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!\*\.!<span style='color: red'><i>&lt;anything&gt;</i></span>.!;
my $vars = {
disp_idurl => LJ::ehtml( $args->{'identity'} ),
disp_site => $disp_site,
};
return DW::Template->render_template( 'openid/approve.tt', $vars );
}
1;