File Coverage

blib/lib/Catalyst/Controller/RequestToken.pm
Criterion Covered Total %
statement 1 3 33.3
branch n/a
condition n/a
subroutine 1 1 100.0
pod n/a
total 2 4 50.0


line stmt bran cond sub pod time code
1             package Catalyst::Controller::RequestToken;
2 2     2   59074 use Moose;
  0            
  0            
3             BEGIN { extends 'Catalyst::Controller' }
4              
5             use Digest;
6             use Catalyst::Exception;
7             use namespace::autoclean;
8              
9             our $VERSION = '0.07';
10              
11             has [qw/ session_name request_name /] => (
12             is => 'ro',
13             default => '_token'
14             );
15              
16             sub BUILD {
17             my $self = shift;
18              
19             Catalyst::Exception->throw("Catalyst::Plugin::Session is required")
20             unless $self->_application->isa('Catalyst::Plugin::Session');
21             }
22              
23             sub token {
24             my ( $self, $ctx, $arg ) = @_;
25              
26             confess("ARGH") unless $ctx && blessed($ctx);
27             if ( defined $arg ) {
28             $ctx->session->{ $self->_ident() } = $arg;
29             return $arg;
30             }
31              
32             return $ctx->session->{ $self->_ident() };
33             }
34              
35             sub create_token {
36             my ( $self, $c, $arg ) = @_;
37              
38             $c->log->debug("create token") if $c->debug;
39             my $digest = _find_digest();
40             my $seed = join( time, rand(10000), $$, {} );
41             $digest->add($seed);
42             my $token = $digest->hexdigest;
43             $c->log->debug("token is created: $token") if $c->debug;
44              
45             return $self->token($c, $token);
46             }
47              
48             sub remove_token {
49             my ( $self, $c, $arg ) = @_;
50              
51             $c->log->debug("remove token") if $c->debug;
52             undef $c->session->{ $self->_ident() };
53             $self->token($c, undef);
54             }
55              
56             sub validate_token {
57             my ( $self, $c, $arg ) = @_;
58              
59             $c->log->debug('validate token') if $c->debug;
60             my $session = $self->token($c);
61             my $request = $c->req->param( $self->{request_name} );
62              
63             $c->log->debug( "session:" . ( $session ? $session : '' ) ) if $c->debug;
64             $c->log->debug( "request:" . ( $request ? $request : '' ) ) if $c->debug;
65              
66             if ( ( $session && $request ) && $session eq $request ) {
67             $c->log->debug('token is valid') if $c->debug;
68             $c->stash->{ $self->_ident() } = 1;
69             }
70             else {
71             $c->log->debug('token is invalid') if $c->debug;
72             if ( $c->isa('Catalyst::Plugin::FormValidator::Simple') ) {
73             $c->set_invalid_form( $self->{request_name} => 'TOKEN' );
74             }
75             undef $c->stash->{ $self->_ident() };
76             }
77             }
78              
79             sub is_valid_token {
80             my ( $self, $ctx, $arg ) = @_;
81              
82             confess("ARGH") unless blessed($ctx);
83             return $ctx->stash->{ $self->_ident() };
84             }
85              
86             sub _ident { # secret stash key for this template'
87             return '__' . ref( $_[0] ) . '_token';
88             }
89              
90             # following code is from Catalyst::Plugin::Session
91             my $usable;
92              
93             sub _find_digest () {
94             unless ($usable) {
95             foreach my $alg (qw/SHA-256 SHA-1 MD5/) {
96             if ( eval { Digest->new($alg) } ) {
97             $usable = $alg;
98             last;
99             }
100             }
101             Catalyst::Exception->throw(
102             "Could not find a suitable Digest module. Please install "
103             . "Digest::SHA1, Digest::SHA, or Digest::MD5" )
104             unless $usable;
105             }
106              
107             return Digest->new($usable);
108             }
109              
110             sub _parse_CreateToken_attr {
111             my ( $self, $app_class, $action_name, $vaue, $attrs ) = @_;
112              
113             return ( ActionClass =>
114             'Catalyst::Controller::RequestToken::Action::CreateToken' );
115             }
116              
117             sub _parse_ValidateToken_attr {
118             my ( $self, $app_class, $action_name, $vaue, $attrs ) = @_;
119              
120             return ( ActionClass =>
121             'Catalyst::Controller::RequestToken::Action::ValidateToken' );
122             }
123              
124             sub _parse_RemoveToken_attr {
125             my ( $self, $app_class, $action_name, $vaue, $attrs ) = @_;
126              
127             return ( ActionClass =>
128             'Catalyst::Controller::RequestToken::Action::RemoveToken' );
129             }
130              
131             sub _parse_ValidateRemoveToken_attr {
132             my ( $self, $app_class, $action_name, $vaue, $attrs ) = @_;
133              
134             return ( ActionClass =>
135             'Catalyst::Controller::RequestToken::Action::ValidateRemoveToken'
136             );
137             }
138              
139             1;
140              
141             __END__
142              
143             =head1 NAME
144              
145             Catalyst::Controller::RequestToken - Handling transaction tokens across forms
146              
147             =head1 SYNOPSIS
148              
149             requires Catalyst::Plugin::Session module, in your application class:
150              
151             use Catalyst qw/
152             Session
153             Session::State::Cookie
154             Session::Store::FastMmap
155             FillInForm
156             /;
157              
158             in your controller class:
159              
160             use base qw(Catalyst::Controller::RequestToken);
161              
162             sub form :Local {
163             my ($self, $c) = @_;
164             $c->stash( template => 'form.tt' );
165             }
166              
167             sub confirm :Local :CreateToken {
168             my ($self, $c) = @_;
169             $c->stash( template => 'confirm.tt' );
170             }
171              
172             sub complete :Local :ValidateToken {
173             my ($self, $c) = @_;
174              
175             if ($self->valid_token($c)) {
176             $c->response->body('complete.');
177             }
178             eles {
179             $c->response->body('invalid operation.');
180             }
181             }
182              
183             form.tt
184              
185             <html>
186             <body>
187             <form action="confirm" method="post">
188             <input type="submit" name="submit" value="confirm"/>
189             </form>
190             </body>
191             </html>
192              
193             confirm.tt
194              
195             <html>
196             <body>
197             <form action="complete" method="post">
198             <input type="hidden" name="_token" values="[% c.req.param('_token') %]"/>
199             <input type="submit" name="submit" value="complete"/>
200             </form>
201             </body>
202             </html>
203              
204             =head1 DESCRIPTION
205              
206             This controller enables to enforce a single transaction across multiple forms.
207             Using a token, you can prevent duplicate submits and protect your app from CSRF atacks.
208              
209             This module REQUIRES Catalyst::Plugin::Session to store server side token.
210              
211             =head1 ATTRIBUTES
212              
213             =over 4
214              
215             =item CreateToken
216              
217             Creates a new token and puts it into request and session.
218             You can return content with request token which should be posted
219             to server.
220              
221             =item ValidateToken
222              
223             After CreateToken, clients will post token request, so you need to
224             validate whether it is correct or not.
225              
226             The ValidateToken attribute wil make your action validate the request token
227             by comparing it to the session token which is created by the CreateToken attribute.
228              
229             If the token is valid, the server-side token will be expired. Use is_valid_token()
230             to check wheter the token in this request was valid or not.
231              
232             =item RemoveToken
233              
234             Removes the token from the session. The request token will no longer be valid.
235              
236             =back
237              
238             =head1 METHODS
239              
240             All methods must be passed the request context as their first parameter.
241              
242             =over 4
243              
244             =item token
245              
246             =item create_token
247              
248             =item remove_token
249              
250             =item validate_token
251              
252             Return whether token is valid or not. This will work correctly only after
253             ValidateToken.
254              
255             =item is_valid_token
256              
257             =back
258              
259             =head1 CONFIGRATION
260              
261             in your application class:
262              
263             __PACKAGE__->config('Controller::TokenBasedMyController' => {
264             session_name => '_token',
265             request_name => '_token',
266             });
267              
268             =over 4
269              
270             =item session_name
271              
272             Default: _token
273              
274             =item request_name
275              
276             Default: _token
277              
278             =item validate_stash_name
279              
280             Default: _token
281              
282             =back
283              
284              
285             =head1 SEE ALSO
286              
287             =over
288              
289             =item L<Catalyst::Controller::RequestToken::Action::CreateToken>
290              
291             =item L<Catalyst::Controller::RequestToken::Action::ValidateToken>
292              
293             =item L<Catalyst>
294              
295             =item L<Catalyst::Controller>
296              
297             =item L<Catalyst::Plugin::Session>
298              
299             =item L<Catalyst::Plugin::FormValidator::Simple>
300              
301             =back
302              
303             =head1 AUTHOR
304              
305             Hideo Kimura C<< <<hide<at>hide-k.net>> >>
306              
307             =head1 COPYRIGHT
308              
309             This program is free software; you can redistribute
310             it and/or modify it under the same terms as Perl itself.
311              
312             The full text of the license can be found in the
313             LICENSE file included with this module.
314              
315             =cut
316