File Coverage

blib/lib/OpenID/Lite/Provider.pm
Criterion Covered Total %
statement 7 9 77.7
branch n/a
condition n/a
subroutine 3 3 100.0
pod n/a
total 10 12 83.3


line stmt bran cond sub pod time code
1             package OpenID::Lite::Provider;
2              
3 1     1   5 use Any::Moose;
  1         2  
  1         6  
4 1     1   432 use OpenID::Lite::Message;
  1         2  
  1         7  
5 1     1   49 use OpenID::Lite::Realm;
  0            
  0            
6             use OpenID::Lite::Provider::Discover;
7             use OpenID::Lite::Provider::Response;
8             use OpenID::Lite::Provider::AssociationBuilder;
9             use OpenID::Lite::Provider::Handler::Association;
10             use OpenID::Lite::Provider::Handler::CheckAuth;
11             use OpenID::Lite::Provider::Handler::CheckID;
12             use OpenID::Lite::Constants::ModeType qw(:all);
13             use OpenID::Lite::Constants::Namespace qw(:all);
14             use OpenID::Lite::Constants::ProviderResponseType qw(:all);
15              
16             with 'OpenID::Lite::Role::ErrorHandler';
17             with 'OpenID::Lite::Role::AgentHandler';
18              
19             has 'secret_lifetime' => (
20             is => 'rw',
21             isa => 'Int',
22             default => 14 * 24 * 60 * 60,
23             );
24              
25             has 'server_secret' => (
26             is => 'ro',
27             isa => 'Str',
28             default => q{secret},
29             );
30              
31             #has 'secret_gen_interval' => (
32             # is => 'rw',
33             # isa => 'Int',
34             # default => 14 * 24 * 60 * 60,
35             #);
36             #
37             #has 'get_server_secret' => (
38             # is => 'ro',
39             # isa => 'CodeRef',
40             # default => sub {
41             # sub { return ''; }
42             # },
43             #);
44             #
45             has 'setup_url' => (
46             is => 'rw',
47             isa => 'Str',
48             required => 1,
49             );
50              
51             has 'endpoint_url' => (
52             is => 'rw',
53             isa => 'Str',
54             required => 1,
55             );
56              
57             has 'get_user' => (
58             is => 'ro',
59             isa => 'CodeRef',
60             default => sub {
61             sub { return; }
62             },
63             );
64              
65             has 'get_identity' => (
66             is => 'ro',
67             isa => 'CodeRef',
68             default => sub {
69             sub { return; }
70             },
71             );
72              
73             has 'is_identity' => (
74             is => 'ro',
75             isa => 'CodeRef',
76             default => sub {
77             sub { return; }
78             },
79             );
80              
81             has 'is_trusted' => (
82             is => 'ro',
83             isa => 'CodeRef',
84             default => sub {
85             sub { return; }
86             },
87             );
88              
89             has '_discoverer' => (
90             is => 'ro',
91             isa => 'OpenID::Lite::Provider::Discover',
92             lazy_build => 1,
93             );
94              
95             has '_assoc_builder' => (
96             is => 'ro',
97             isa => 'OpenID::Lite::Provider::AssociationBuilder',
98             lazy_build => 1,
99             );
100              
101             has '_handlers' => (
102             is => 'ro',
103             isa => 'HashRef',
104             lazy_build => 1,
105             );
106              
107             sub handle_request {
108             my ( $self, $request ) = @_;
109             my $params = OpenID::Lite::Message->from_request($request);
110             my $mode = $params->get('mode');
111             return $self->ERROR(q{Missing parameter, "mode"}) unless $mode;
112             my $handler = $self->_get_handler_for($mode);
113             return $self->ERROR( sprintf q{Invalid paramter, "mode", "%s"}, $mode )
114             unless $handler;
115             my $result = $handler->handle_request($params)
116             or return $self->ERROR( $handler->errstr );
117             return $result;
118             }
119              
120             sub _get_handler_for {
121             my ( $self, $mode ) = @_;
122             if ( $mode eq ASSOCIATION ) {
123             return $self->_handlers->{associate};
124             }
125             elsif ( $mode eq CHECK_AUTHENTICATION ) {
126             return $self->_handlers->{checkauth};
127             }
128             elsif ($mode eq CHECKID_SETUP
129             || $mode eq CHECKID_IMMEDIATE )
130             {
131             return $self->_handlers->{checkid};
132             }
133             return;
134             }
135              
136             sub _build__handlers {
137             my $self = shift;
138             my $handlers = {};
139             $handlers->{associate}
140             = OpenID::Lite::Provider::Handler::Association->new(
141             assoc_builder => $self->_assoc_builder, );
142              
143             $handlers->{checkauth} = OpenID::Lite::Provider::Handler::CheckAuth->new(
144             assoc_builder => $self->_assoc_builder, );
145              
146             $handlers->{checkid} = OpenID::Lite::Provider::Handler::CheckID->new(
147             assoc_builder => $self->_assoc_builder,
148             setup_url => $self->setup_url,
149             endpoint_url => $self->endpoint_url,
150             get_user => $self->get_user,
151             get_identity => $self->get_identity,
152             is_identity => $self->is_identity,
153             is_trusted => $self->is_trusted,
154             );
155             return $handlers;
156             }
157              
158             sub _build__assoc_builder {
159             my $self = shift;
160             my $assoc_builder = OpenID::Lite::Provider::AssociationBuilder->new(
161             server_secret => $self->server_secret,
162             secret_lifetime => $self->secret_lifetime,
163             # secret_gen_interval => $self->secret_gen_interval,
164             # get_server_secret => $self->get_server_secret,
165             );
166             return $assoc_builder;
167             }
168              
169             # my $req = $op->make_op_initiated_assertion( $rp_realm, $user_identifier )
170             # or $your_app->error( $op->errstr );
171             # $your_app->redirect( $req->make_singed_url() );
172              
173             sub make_op_initiated_assertion {
174             my ( $self, $rp_realm, $identifier ) = @_;
175             my $urls = $self->discover_rp($rp_realm)
176             or return;
177             return $self->ERROR( sprintf q{url not found for realm, "%s"}, $rp_realm )
178             unless @$urls > 0;
179             return $self->make_op_initiated_assertion_without_discovery( $rp_realm,
180             $urls->[0], $identifier );
181             }
182              
183             sub make_op_initiated_assertion_without_discovery {
184             my ( $self, $rp_realm, $url, $identifier ) = @_;
185              
186             my $message = OpenID::Lite::Message->new;
187             $message->set( ns => SIGNON_2_0 );
188             $message->set( realm => $rp_realm );
189             $message->set( claimed_id => $identifier );
190             $message->set( identity => $identifier );
191             $message->set( return_to => $url );
192              
193             return OpenID::Lite::Provider::Response->new(
194             type => POSITIVE_ASSERTION,
195             req_params => $message,
196             res_params => $message,
197             assoc_builder => $self->_assoc_builder,
198             endpoint_url => $self->endpoint_url,
199             );
200             }
201              
202             sub discover_rp {
203             my ( $self, $rp_realm ) = @_;
204             unless ( ref($rp_realm) eq 'OpenID::Lite::Realm' ) {
205             $rp_realm = OpenID::Lite::Realm->parse($rp_realm)
206             or
207             return $self->ERROR( sprintf q{Invalid realm "%s"}, $rp_realm );
208             }
209             my $return_to_urls
210             = $self->_discoverer->discover( $rp_realm->build_discovery_url )
211             or return $self->ERROR( $self->_discover->errstr );
212             return $return_to_urls;
213             }
214              
215             sub _build__discoverer {
216             my $self = shift;
217             return OpenID::Lite::Provider::Discover->new( agent => $self->agent );
218             }
219              
220             no Any::Moose;
221             __PACKAGE__->meta->make_immutable;
222             1;
223              
224             =head1 NAME
225              
226             OpenID::Lite::Provider - OpenID Provider support module
227              
228             =head1 SYNOPSIS
229              
230             OpenID Controller
231              
232             package YourApp::OpenIDController;
233              
234             my $op = OpenID::Lite::Provider->new(
235             endpoint_url => q{http://yourapp.com/openid},
236             setup_url => q{http://yourapp.com/setup},
237             server_secret => q{SECRETKEY},
238             );
239              
240             # server endpoint
241             sub openid {
242              
243             my $your_app = shift;
244              
245             my $result = $op->handle_request( $your_app->request );
246              
247             if ( !$result ) {
248              
249             # error occured
250             # invalid as openid-request.
251             $your_app->view->content_type('text/plain');
252             $your_app->view->content($op->errstr);
253             return;
254              
255             } elsif ( $result->is_for_setup ) {
256              
257             # save the parameters into session
258             # this is just an example, you can take other ways.
259             # for example, use query-string parameter.
260             $your_app->session->set( 'openid.checkid' => $result );
261              
262             # required setup and
263             # show decision page
264              
265             # Case 1. redirect to action that is for setup
266             $your_app->redirect_to( $your_app->uri_to( action => 'setup' ) );
267             return;
268              
269             # Case 2. or directly show setup page.
270             $your_app->view->render('decision_page', {
271             realm => $result->get_realm(),
272             } );
273              
274             } elsif ( $result->requires_setup ) {
275              
276             # RP requested as immediate-mode, but your app (provider)
277             # doesn't accept immediate mode.
278             return $your_app->redirect_to( $result->make_setup_url() );
279              
280             } elsif ( $result->is_positive_assertion ) {
281              
282             # successfully done as immediate-mode.
283              
284             # execute extension processes here if you need.
285             my $sreg_req = OpenID::Lite::Extension::SREG::Request->from_provider_response($result);
286             my $user_data = $self->session->get('user');
287             my $sreg_data = {
288             nickname => $user_data->nickname,
289             fullname => $user_data->fullname,
290             email => $user_data->email,
291             };
292             my $sreg_res = OpenID::Lite::Extension::SREG::Response->extract_response($sreg_req, $sreg_data);
293             $result->add_extension( $sreg_res );
294              
295             # redirect back to RP with successful signed params.
296             return $self->redirect_to( $result->make_signed_url() );
297              
298             } elsif ( $result->is_for_direct_communication ) {
299              
300             # direct communication response
301             # This case is for establishing association and checking auth.
302             $self->view->content( $result->content );
303             return;
304              
305             } elsif ( $result->is_checkid_error ) {
306              
307             return $self->redirect_to( $self->make_error_url() );
308              
309             }
310             }
311              
312             # action that shows decision page.
313             sub setup {
314             my $self = shift;
315             my $checkid_result = $self->session->get('openid.checkid');
316             }
317              
318             # if user canceled to approve RP request.
319             sub user_cancel {
320             my $self = shift;
321             my $checkid_result = $self->session->get('openid.checkid');
322             return $self->redirect_to( $checkid_result->make_cancel_url() );
323             }
324              
325             # if user approved RP request.
326             sub user_approved {
327             my $self = shift;
328              
329             my $checkid_result = $self->session->get('openid.checkid')
330             or return $self->show_error('Invalid openid-session');
331              
332             # RETURN POSITIVE ASSERTION
333             # redirect to RP as positive-assertion
334              
335             # execute extension processes here if you need.
336             my $sreg_req = OpenID::Lite::Extension::SREG::Request->from_provider_response($checkid_result);
337             my $user_data = $self->session->get('user');
338             my $sreg_data = {
339             nickname => $user_data->nickname,
340             fullname => $user_data->fullname,
341             email => $user_data->email,
342             };
343             my $sreg_res = OpenID::Lite::Extension::SREG::Response->extract_response($sreg_req, $sreg_data);
344             $checkid_result->add_extension( $sreg_res );
345             return $self->redirect_to( $checkid_result->make_signed_url() );
346             }
347              
348             1;
349              
350             Application Root
351              
352             package YourApp::RootController;
353              
354             sub root {
355             my $self = shift;
356             if ( $self->req->header('Accept') =~ m!application/xrds+xml!i ) {
357             print_xrds();
358             return;
359             }
360             }
361             1;
362              
363             User Page
364              
365             package YourApp::UserController;
366              
367             sub user {
368             my ( $self, $user_id ) = @_;
369             if ( $self->req->header('Accept') =~ m!application/xrds+xml!i ) {
370             print_claimed_id_xrds($user_id);
371             return;
372             }
373             }
374              
375             1;
376              
377              
378             =head1 DESCRIPTION
379              
380             This moduel allows you to mae OpenID Provider easily.
381             This supports OpenID 2.0.
382              
383             'Lite' means nothing. It's to escape namespace confliction.
384              
385             =head1 SETUP
386              
387             my $op = OpenID::Lite::Provider->new(
388             endpoint_url => q{http://yourapp.com/openid},
389             setup_url => q{http://yourapp.com/setup},
390             );
391              
392             =head2 new
393              
394             parameters
395              
396             =over 4
397              
398             =item setup_url
399              
400             The OpenID setup url.
401              
402             =item endpoint_url
403              
404             The OpenID endpoint url.
405              
406             =item server_secret
407              
408             Secret string to generate association.
409              
410             =item secret_lifetime
411              
412             Lifetime seconds for each association.
413              
414             =item agent
415              
416             Used for RP discovery.
417              
418             See L, L
419             L, L
420              
421             =back
422              
423             Callback functions
424             You can set callbacks, then they will be able to automatically
425             controll to judge approve request from RP or not.
426             If you want to manually handle request,
427             see 'REQUEST HANDLING' section.
428              
429             =over 4
430              
431             =item get_user
432              
433             Callback function to get current user object.
434             Other callback functions uses the returned user object.
435              
436             my $your_app = ...;
437             get_user => sub {
438             return $your_app->session->get('user');
439             }
440              
441             =item get_identity
442              
443             Callback function to get user identity.
444             If your app provieds users multiple identifier for each realm,
445             use the second arg.
446              
447             get_identity => sub {
448             my ( $user, $realm ) = @_;
449              
450             # if your app provides users with only single identifier
451             return $user->get_identity();
452              
453             # if your app provides users with diffirent identifier for each realm.
454             return $user->get_identity_for($realm);
455             }
456              
457             =item is_identity
458              
459             Callback function that checks the passed identity is
460             for indicated user's one or not.
461             If your app provieds users multiple identifier for each realm,
462             use the third arg.
463              
464             is_identity => sub {
465             my ( $user, $identity, $realm ) = @_;
466             return ( $user->get_identity_for($realm) eq $identity ) ? 1 : 0;
467             }
468              
469             =item is_trusted
470              
471             Callback function that checks that if the current user trusts
472             requesting RP or not.
473              
474             is_trusted => sub {
475             my ( $user, $realm ) = @_;
476             return $user->trust( $realm ) ? 1 : 0;
477             }
478              
479             =back
480              
481             =head1 REQUEST HANDLING
482              
483             execute handle_reuqest method, and
484             switch process properly for each result type.
485              
486             my $result = $op->handle_request( $your_app->request );
487             if ( !$result ) {
488             # error
489             } elsif ( ... ) {
490              
491             } elsif ( ... ) {
492              
493             } elsif ( ... ) {
494              
495             }
496              
497             =head2 NOT FOUND RESULT
498              
499             If $op->handle_request returns nothing.
500             You can pick the error string from $op->errstr method.
501              
502             if ( !$result ) {
503             $your_app->log( $op->errstr );
504             $your_app->show_error( q{ Invalid openid request.} );
505             }
506              
507             =head2 POSITIVE ASSERTION
508              
509             When OP accept the case like that, user had already approved the requesting RP,
510             returns positive assertion directly without displaying dicision page.
511             To accomplish this, you have to set callback functions(get_user, get_identity, and so on)
512             when calling 'new' method.
513              
514              
515              
516             } elsif ( $result->is_positive_assertion ) {
517              
518             $your_app->redirect( $result->make_signed_url() );
519             } ...
520              
521             And if you need support extension.
522             Do their process here, or SETUP phase discribed bellow.
523              
524             } elsif ( $result->is_positive_assertion ) {
525              
526             my $sreg_req = OpenID::Lite::Extension::SREG::Request->from_provider_response( $result );
527             my $user_data = $self->session->get('user');
528             my $sreg_data = {
529             nickname => $user_data->nickname,
530             fullname => $user_data->fullname,
531             email => $user_data->email,
532             };
533             my $sreg_res = OpenID::Lite::Extension::SREG::Response->extract_response($sreg_req, $sreg_data);
534             $result->add_extension( $sreg_res );
535              
536             $your_app->redirect( $result->make_signed_url() );
537             } ...
538              
539              
540             =head2 SETUP
541              
542             When RP requests checkid not-immediate request,
543             and no error found.( for the case if error found, see CHECKID ERROR section ).
544              
545             Normally, you can choose two ways here,
546             Show dicision page directly, or redirect user to setup-url.
547              
548             And to show some information to users.
549             You can pick them from result object.
550             see get_relam, get_claimed_id, get_identity methods bellow.
551              
552             It is better to save result information into session
553             until user will be back with setup completion action or canceling action.
554             In those actions, result object will be required.
555              
556             And you can set identifier for user here with
557             'set_identity' method of result object.
558              
559              
560             1. Redirecting case.
561              
562             } elsif ( $result->is_for_setup ) {
563              
564             $your_app->session->save( 'openid' => $result );
565              
566             $your_app->redirect( $result->make_setup_url() );
567              
568             # or manually make url by yourself.
569             #$your_app->redirect( $your_app->uri_for(
570             # action => 'setup',
571             #) );
572             }
573              
574              
575             2. Show dicision page case.
576              
577             } elsif ( $result->is_for_setup ) {
578              
579             my $realm = $result->get_realm();
580              
581             # if you set get_identity callback to Provider object,
582             # you may get identity by this method.
583             my $identity = $result->get_identity();
584              
585             # or set manually here.
586             # my $identity = $your_app->build_user_identity(
587             # $your_app->session->get('user')->id );
588             # $result->set_identity( $identity );
589              
590             $your_app->session->save( 'openid' => $result );
591              
592             $your_app->show_dicition_page(
593             realm => $realm,
594             identity => $identity,
595             );
596             } ...
597              
598             And as described on POSITIVE ASSERTION phase,
599             You can extract information for extension.
600              
601             } elsif ( $result->is_for_setup ) {
602              
603             my $realm = $result->get_realm();
604              
605             # if you set get_identity callback to Provider object,
606             # you may get identity by this method.
607             my $identity = $result->get_identity();
608              
609             # or set manually here.
610             # my $identity = $your_app->build_user_identity(
611             # $your_app->session->get('user')->id );
612             # $result->set_identity( $identity );
613              
614             $your_app->session->save( 'openid' => $result );
615              
616             my $sreg_req = OpenID::Lite::Extension::SREG::Request->from_provider_response( $result );
617              
618             my $fields = $sreg_req->all_requested_fields();
619             my $message = '';
620             if ( @$fields > 0 ) {
621             $message = sprintf(q{the RP requests your fields, "%s"},
622             join(', ', @$fields) );
623             }
624              
625             my $template = 'decision_page.tt';
626              
627             my $ui_req = OpenID::Lite::Extension::UI::Request->from_provider_response( $result );
628             if ( $ui_req->mode eq 'popup' ) {
629             $template = 'decision_page_for_popup.tt';
630             }
631              
632             $your_app->show_dicition_page(
633             template => $template,
634             realm => $realm,
635             identity => $identity,
636             message => $message,
637             );
638             } ...
639              
640             =head2 REQUIRES SETUP
641              
642             RP send checkid-request but OP doesn't accept immedate mode.
643             OP should let RP know setup-url.
644              
645             } elsif ( $result->requires_setup ) {
646             $your_app->redirect( $result->make_setup_url() );
647             } ...
648              
649             =head2 DIRECT COMMUNICATION
650              
651             For establishing association or CheckAuth request.
652             Directly print key-value form encoded content.
653              
654             } elsif ( $result->is_direct_communication ) {
655             $your_app->view->content( $result->content );
656             } ...
657              
658             =head2 CHECKID ERROR
659              
660             If any error occured while processing checkid-request,
661             You should redirect user back to RP with openid-error parameters.
662              
663             } elsif ( $result->is_checkid_error ) {
664             $your_app->redirect( $result->make_error_url() );
665             } ...
666              
667             =head1 OP INITIATE
668              
669             execute discovery and find return_to url by realm.
670             But it works only when RP implements XRDS publishing correctly for realm.
671              
672             my $assertion = $op->make_op_initiated_assertion(
673             $rp_realm,
674             $current_user_identifier,
675             ) or $your_app->error( $op->errstr );
676              
677             Or if you already know the return_to url corresponding to the realm.
678             You can make assertion without discovery.
679              
680             my $assertion = $op->make_op_initiated_assertion_without_discovery(
681             $rp_realm,
682             $rp_return_to,
683             $current_user_identifier,
684             ) or $your_app->error( $op->errstr );
685              
686             If you need, add extension here
687              
688             my $ext_res = OpenID::Lite::Extension::Something::Response->new;
689             $ext_res->add_some_param( foo => 'bar' );
690             $assertion->add_extension( $ext_res );
691              
692             And finally, build signed url to redirect with it.
693              
694             $your_app->redirect( $assertion->make_signed_url() );
695              
696             =head1 SEE ALSO
697              
698             http://openid.net/specs/openid-authentication-2_0.html
699             http://openidenabled.com/
700              
701             =head2 TODO
702              
703             =over 4
704              
705             =item Improve an interoperability with majour services.
706              
707             =back
708              
709             =head1 AUTHOR
710              
711             Lyo Kato, Elyo.kato@gmail.comE
712              
713             =head1 COPYRIGHT AND LICENSE
714              
715             Copyright (C) 2009 by Lyo Kato
716              
717             This library is free software; you can redistribute it and/or modify
718             it under the same terms as Perl itself, either Perl version 5.8.8 or,
719             at your option, any later version of Perl 5 you may have available.
720              
721             =cut