X-Git-Url: http://git.vanrenterghem.biz/git.ikiwiki.info.git/blobdiff_plain/42ac4ec00959bd6162076af799023545b698bed9..2130e63f303112fced7ed3ae1977351b762b4e44:/IkiWiki/Plugin/openid.pm diff --git a/IkiWiki/Plugin/openid.pm b/IkiWiki/Plugin/openid.pm index de7f7280e..d369e30c9 100644 --- a/IkiWiki/Plugin/openid.pm +++ b/IkiWiki/Plugin/openid.pm @@ -4,103 +4,122 @@ package IkiWiki::Plugin::openid; use warnings; use strict; -use IkiWiki 2.00; +use IkiWiki 3.00; -sub import { #{{{ - hook(type => "getopt", id => "openid", call => \&getopt); +sub import { + add_underlay("openid-selector"); + add_underlay("jquery"); + hook(type => "checkconfig", id => "openid", call => \&checkconfig); hook(type => "getsetup", id => "openid", call => \&getsetup); hook(type => "auth", id => "openid", call => \&auth); hook(type => "formbuilder_setup", id => "openid", call => \&formbuilder_setup, last => 1); -} # }}} +} -sub getopt () { #{{{ - eval q{use Getopt::Long}; - error($@) if $@; - Getopt::Long::Configure('pass_through'); - GetOptions("openidsignup=s" => \$config{openidsignup}); -} #}}} +sub checkconfig () { + if ($config{cgi}) { + # Intercept normal signin form, so the openid selector + # can be displayed. + # + # When other auth hooks are registered, give the selector + # a reference to the normal signin form. + require IkiWiki::CGI; + my $real_cgi_signin; + if (keys %{$IkiWiki::hooks{auth}} > 1) { + $real_cgi_signin=\&IkiWiki::cgi_signin; + } + inject(name => "IkiWiki::cgi_signin", call => sub ($$) { + openid_selector($real_cgi_signin, @_); + }); + } +} -sub getsetup () { #{{{ +sub getsetup () { return - openidsignup => { - type => "string", - example => "http://myopenid.com/", - description => "an url where users can signup for an OpenID", + plugin => { safe => 1, rebuild => 0, + section => "auth", + }, + openid_realm => { + type => "string", + description => "url pattern of openid realm (default is cgiurl)", + safe => 0, + rebuild => 0, }, -} #}}} + openid_cgiurl => { + type => "string", + description => "url to ikiwiki cgi to use for openid authentication (default is cgiurl)", + safe => 0, + rebuild => 0, + }, +} + +sub openid_selector { + my $real_cgi_signin=shift; + my $q=shift; + my $session=shift; -sub formbuilder_setup (@) { #{{{ + my $openid_url=$q->param('openid_identifier'); + my $openid_error; + + if (! load_openid_module()) { + if ($real_cgi_signin) { + $real_cgi_signin->($q, $session); + exit; + } + error(sprintf(gettext("failed to load openid module: "), @_)); + } + elsif (defined $q->param("action") && $q->param("action") eq "verify") { + validate($q, $session, $openid_url, sub { + $openid_error=shift; + }); + } + + my $template=IkiWiki::template("openid-selector.tmpl"); + $template->param( + cgiurl => IkiWiki::cgiurl(), + (defined $openid_error ? (openid_error => $openid_error) : ()), + (defined $openid_url ? (openid_url => $openid_url) : ()), + ($real_cgi_signin ? (nonopenidform => $real_cgi_signin->($q, $session, 1)) : ()), + ); + + IkiWiki::printheader($session); + print IkiWiki::cgitemplate($q, "signin", $template->output); + exit; +} + +sub formbuilder_setup (@) { my %params=@_; my $form=$params{form}; my $session=$params{session}; my $cgi=$params{cgi}; - if ($form->title eq "signin") { - # Give up if module is unavailable to avoid - # needing to depend on it. - eval q{use Net::OpenID::Consumer}; - if ($@) { - debug("unable to load Net::OpenID::Consumer, not enabling OpenID login"); - return; - } - - # This avoids it displaying a redundant label for the - # OpenID fieldset. - $form->fieldsets("OpenID"); - - $form->field( - name => "openid_url", - label => gettext("Log in with")." ".htmllink("", "", "ikiwiki/OpenID", noimageinline => 1), - fieldset => "OpenID", - size => 30, - comment => ($config{openidsignup} ? " | ".gettext("Get an OpenID")."" : "") - ); - - # Handle submission of an OpenID as validation. - if ($form->submitted && $form->submitted eq "Login" && - defined $form->field("openid_url") && - length $form->field("openid_url")) { - $form->field( - name => "openid_url", - validate => sub { - validate($cgi, $session, shift, $form); - }, - ); - # Skip all other required fields in this case. - foreach my $field ($form->field) { - next if $field eq "openid_url"; - $form->field(name => $field, required => 0, - validate => '/.*/'); - } - } - } - elsif ($form->title eq "preferences") { - if (! defined $form->field(name => "name")) { - $form->field(name => "OpenID", disabled => 1, - value => $session->param("name"), - size => 50, force => 1, - fieldset => "login"); - } + if ($form->title eq "preferences" && + IkiWiki::openiduser($session->param("name"))) { + $form->field(name => "openid_identifier", disabled => 1, + label => htmllink("", "", "ikiwiki/OpenID", noimageinline => 1), + value => "", + size => 1, force => 1, + fieldset => "login", + comment => $session->param("name")); + $form->field(name => "email", type => "hidden"); } } -sub validate ($$$;$) { #{{{ +sub validate ($$$;$) { my $q=shift; my $session=shift; my $openid_url=shift; - my $form=shift; + my $errhandler=shift; my $csr=getobj($q, $session); my $claimed_identity = $csr->claimed_identity($openid_url); if (! $claimed_identity) { - if ($form) { - # Put the error in the form and fail validation. - $form->field(name => "openid_url", comment => $csr->err); + if ($errhandler) { + $errhandler->($csr->err); return 0; } else { @@ -108,18 +127,46 @@ sub validate ($$$;$) { #{{{ } } + # Ask for client to provide a name and email, if possible. + # Try sreg and ax + if ($claimed_identity->can("set_extension_args")) { + $claimed_identity->set_extension_args( + 'http://openid.net/extensions/sreg/1.1', + { + optional => 'email,fullname,nickname', + }, + ); + $claimed_identity->set_extension_args( + 'http://openid.net/srv/ax/1.0', + { + mode => 'fetch_request', + 'required' => 'email,fullname,nickname,firstname', + 'type.email' => "http://schema.openid.net/contact/email", + 'type.fullname' => "http://axschema.org/namePerson", + 'type.nickname' => "http://axschema.org/namePerson/friendly", + 'type.firstname' => "http://axschema.org/namePerson/first", + }, + ); + } + + my $cgiurl=$config{openid_cgiurl}; + $cgiurl=$q->url if ! defined $cgiurl; + + my $trust_root=$config{openid_realm}; + $trust_root=$cgiurl if ! defined $trust_root; + my $check_url = $claimed_identity->check_url( - return_to => IkiWiki::cgiurl(do => "postsignin"), - trust_root => $config{cgiurl}, + return_to => auto_upgrade_https($q, "$cgiurl?do=postsignin"), + trust_root => auto_upgrade_https($q, $trust_root), delayed_return => 1, ); # Redirect the user to the OpenID server, which will # eventually bounce them back to auth() IkiWiki::redirect($q, $check_url); exit 0; -} #}}} +} -sub auth ($$) { #{{{ +sub auth ($$) { my $q=shift; my $session=shift; @@ -130,10 +177,45 @@ sub auth ($$) { #{{{ IkiWiki::redirect($q, $setup_url); } elsif ($csr->user_cancel) { - IkiWiki::redirect($q, $config{url}); + IkiWiki::redirect($q, IkiWiki::baseurl(undef)); } elsif (my $vident = $csr->verified_identity) { $session->param(name => $vident->url); + + my @extensions; + if ($vident->can("signed_extension_fields")) { + @extensions=grep { defined } ( + $vident->signed_extension_fields('http://openid.net/extensions/sreg/1.1'), + $vident->signed_extension_fields('http://openid.net/srv/ax/1.0'), + ); + } + my $nickname; + foreach my $ext (@extensions) { + foreach my $field (qw{value.email email}) { + if (exists $ext->{$field} && + defined $ext->{$field} && + length $ext->{$field}) { + $session->param(email => $ext->{$field}); + if (! defined $nickname && + $ext->{$field}=~/(.+)@.+/) { + $nickname = $1; + } + last; + } + } + foreach my $field (qw{value.nickname nickname value.fullname fullname value.firstname}) { + if (exists $ext->{$field} && + defined $ext->{$field} && + length $ext->{$field}) { + $nickname=$ext->{$field}; + last; + } + } + } + if (defined $nickname) { + $session->param(nickname => + Encode::decode_utf8($nickname)); + } } else { error("OpenID failure: ".$csr->err); @@ -143,12 +225,13 @@ sub auth ($$) { #{{{ # myopenid.com affiliate support validate($q, $session, $q->param('openid_identifier')); } -} #}}} +} -sub getobj ($$) { #{{{ +sub getobj ($$) { my $q=shift; my $session=shift; + eval q{use Net::INET6Glue::INET_is_INET6}; # may not be available eval q{use Net::OpenID::Consumer}; error($@) if $@; @@ -158,7 +241,7 @@ sub getobj ($$) { #{{{ $ua=LWPx::ParanoidAgent->new; } else { - $ua=LWP::UserAgent->new; + $ua=useragent(); } # Store the secret in the session. @@ -167,40 +250,35 @@ sub getobj ($$) { #{{{ $secret=rand; $session->param(openid_secret => $secret); } + + my $cgiurl=$config{openid_cgiurl}; + $cgiurl=$q->url if ! defined $cgiurl; return Net::OpenID::Consumer->new( ua => $ua, args => $q, consumer_secret => sub { return shift()+$secret }, - required_root => $config{cgiurl}, + required_root => auto_upgrade_https($q, $cgiurl), ); -} #}}} - -package IkiWiki; - -# This is not used by this plugin, but this seems the best place to put it. -# Used elsewhere to pretty-display the name of an openid user. -sub openiduser ($) { #{{{ - my $user=shift; - - if ($user =~ m!^https?://! && - eval q{use Net::OpenID::VerifiedIdentity; 1} && !$@) { - my $oid=Net::OpenID::VerifiedIdentity->new(identity => $user); - my $display=$oid->display; - # Convert "user.somehost.com" to "user [somehost.com]". - if ($display !~ /\[/) { - $display=~s/^(.*?)\.([^.]+\.[a-z]+)$/$1 [$2]/; - } - # Convert "http://somehost.com/user" to "user [somehost.com]". - if ($display !~ /\[/) { - $display=~s/^https?:\/\/(.+)\/([^\/]+)$/$2 [$1]/; - } - $display=~s!^https?://!!; # make sure this is removed - eval q{use CGI 'escapeHTML'}; - error($@) if $@; - return escapeHTML($display); +} + +sub auto_upgrade_https { + my $q=shift; + my $url=shift; + if ($q->https()) { + $url=~s/^http:/https:/i; + } + return $url; +} + +sub load_openid_module { + # Give up if module is unavailable to avoid needing to depend on it. + eval q{use Net::OpenID::Consumer}; + if ($@) { + debug("unable to load Net::OpenID::Consumer, not enabling OpenID login ($@)"); + return; } - return; + return 1; } 1