The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
package Catalyst::Plugin::Authentication::Credential::OpenID;

use strict;
use warnings;
our $VERSION = '0.03';

use Net::OpenID::Consumer;
use LWPx::ParanoidAgent;
use UNIVERSAL::require;

sub setup {
    my $c = shift;
    my $config = $c->config->{authentication}->{openid} ||= {};
    ( $config->{user_class}
        ||=  "Catalyst::Plugin::Authentication::User::Hash" )->require;
    $c->NEXT::setup(@_);
}

sub authenticate_openid {
    my($c, $uri) = @_;

    my $config = $c->config->{authentication}->{openid};

    my $csr = Net::OpenID::Consumer->new(
        ua => LWPx::ParanoidAgent->new,
        args => $c->req->params,
        consumer_secret => sub { $_[0] },
    );

    my @try_params = qw( openid_url openid_identifier claimed_uri );
    if ($uri ||= (grep defined, @{$c->req->params}{@try_params})[0]) {
        my $current = $c->req->uri;
        $current->query(undef); # no query
        my $identity = $csr->claimed_identity($uri)
            or Catalyst::Exception->throw($csr->err);
        my $check_url = $identity->check_url(
            return_to  => $current . '?openid-check=1',
            trust_root => $current,
            delayed_return => 1,
        );
        $c->res->redirect($check_url);
        return 0;
    } elsif ($c->req->param('openid-check')) {
        if (my $setup_url = $csr->user_setup_url) {
            $c->res->redirect($setup_url);
            return 0;
        } elsif ($csr->user_cancel) {
            return 0;
        } elsif (my $identity = $csr->verified_identity) {
            my $user = +{ map { $_ => scalar $identity->$_ }
                qw( url display rss atom foaf declared_rss declared_atom declared_foaf foafmaker ) };

            my $store = $config->{store} || $c->default_auth_store;
            if ( $store
                 and my $store_user
                 = $store->get_user( $user->{url}, $user ) ) {
                $c->set_authenticated($store_user);
            } else {
                $user = $config->{user_class}->new($user);
                $c->set_authenticated($user);
            }
            return 1;
        } else {
            Catalyst::Exception->throw("Error validating identity: " .
                $csr->err);
        }
    } else {
        return 0;
    }
}

1;
__END__

=for stopwords
    Flickr
    OpenID
    TypeKey
    app
    auth
    callback
    foaf
    foafmaker
    plugins
    rss
    url
    URI

=head1 NAME

Catalyst::Plugin::Authentication::Credential::OpenID - OpenID credential for Catalyst::Auth framework

=head1 SYNOPSIS

  use Catalyst qw/
    Authentication
    Authentication::Credential::OpenID
    Session
    Session::Store::FastMmap
    Session::State::Cookie
  /;

  # MyApp.yaml -- optional
  authentication:
    openid:
      use_session: 1
      user_class: MyApp::M::User::OpenID

  # whatever in your Controller pm
  sub default : Private {
      my($self, $c) = @_;
      if ($c->user_exists) { ... }
  }

  sub signin_openid : Local {
      my($self, $c) = @_;

      if ($c->authenticate_openid) {
          $c->res->redirect( $c->uri_for('/') );
      }
  }

  # foo.tt
  <form action="[% c.uri_for('/signin_openid') %]" method="GET">
  <input type="text" name="openid_url" class="openid" />
  <input type="submit" value="Sign in with OpenID" />
  </form>

=head1 DESCRIPTION

Catalyst::Plugin::Authentication::Credential::OpenID is an OpenID
credential for Catalyst::Plugin::Authentication framework.

=head1 METHODS

=over 4

=item authenticate_openid

  $c->authenticate_openid;

Call this method in the action you'd like to authenticate the user via
OpenID. Returns 0 if auth is not successful, and 1 if user is
authenticated.

User class specified with I<user_class> config, which defaults to
Catalyst::Plugin::Authentication::User::Hash, will be instantiated
with the following parameters.

By default, L<authenticate_openid> method looks for claimed URI
parameter from the form field named C<openid_url>,
C<openid_identifier> or C<claimed_uri>. If you want to use another
form field name, call it like:

  $c->authenticate_openid( $c->req->param('myopenid_param') );

=over 8

=item url

=item display

=item rss

=item atom

=item foaf

=item declared_rss

=item declared_atom

=item declared_foaf

=item foafmaker

=back

See L<Net::OpenID::VerifiedIdentity> for details.

=back

=head1 DIFFERENCE WITH Authentication::OpenID

There's already Catalyst::Plugin::Authentication::OpenID
(Auth::OpenID) and this plugin tries to deprecate it.

=over 4

=item *

Don't use this plugin with Auth::OpenID since method names will
conflict and your app won't work.

=item *

Auth::OpenID uses your root path (/) as an authentication callback but
this plugin uses the current path, which means you can use this plugin
with other Credential plugins, like Flickr or TypeKey.

=item *

This plugin is NOT a drop-in replacement for Auth::OpenID, but your
app needs only slight modifications to work with this one.

=item *

This plugin is based on Catalyst authentication framework, which means
you can specify I<user_class> or I<auth_store> in your app config and
this modules does the right thing, like other Credential modules. This
crates new User object if authentication is successful, and works with
Session too.

=back

=head1 AUTHOR

Six Apart, Ltd. E<lt>cpan@sixapart.comE<gt>

=head1 LICENSE

This library is free software; you can redistribute it and/or modify
it under the same terms as Perl itself.

=head1 SEE ALSO

L<Catalyst::Plugin::Authentication::OpenID>, L<Catalyst::Plugin::Authentication::Credential::Flickr>

=cut