Commit changes that were in 1.002
[catagits/Catalyst-Authentication-Credential-HTTP.git] / lib / Catalyst / Authentication / Credential / HTTP.pm
CommitLineData
513d8ab6 1package Catalyst::Authentication::Credential::HTTP;
490754a8 2use base qw/Catalyst::Authentication::Credential::Password/;
d99b7693 3
4use strict;
5use warnings;
6
7use String::Escape ();
8use URI::Escape ();
9use Catalyst ();
10use Digest::MD5 ();
11
513d8ab6 12BEGIN {
13 __PACKAGE__->mk_accessors(qw/_config realm/);
14}
d99b7693 15
490754a8 16our $VERSION = "1.002";
d99b7693 17
513d8ab6 18sub new {
19 my ($class, $config, $app, $realm) = @_;
20
21 my $self = { _config => $config, _debug => $app->debug };
22 bless $self, $class;
23
24 $self->realm($realm);
25
26 my $type = $self->_config->{'type'} ||= 'any';
27
28 if (!grep /$type/, ('basic', 'digest', 'any')) {
29 Catalyst::Exception->throw(__PACKAGE__ . " used with unsupported authentication type: " . $type);
30 }
31 return $self;
d99b7693 32}
33
513d8ab6 34sub authenticate {
35 my ( $self, $c, $realm, $auth_info ) = @_;
36 my $auth;
d99b7693 37
513d8ab6 38 $auth = $self->authenticate_digest($c, $realm, $auth_info) if $self->_is_http_auth_type('digest');
39 return $auth if $auth;
d99b7693 40
513d8ab6 41 $auth = $self->authenticate_basic($c, $realm, $auth_info) if $self->_is_http_auth_type('basic');
42 return $auth if $auth;
43
44 $self->authorization_required_response($c, $realm, $auth_info);
45 die $Catalyst::DETACH;
d99b7693 46}
47
48sub authenticate_basic {
513d8ab6 49 my ( $self, $c, $realm, $auth_info ) = @_;
d99b7693 50
51 $c->log->debug('Checking http basic authentication.') if $c->debug;
52
53 my $headers = $c->req->headers;
54
55 if ( my ( $username, $password ) = $headers->authorization_basic ) {
513d8ab6 56 my $user_obj = $realm->find_user( { username => $username }, $c);
57 if (ref($user_obj)) {
490754a8 58 if ($self->check_password($user_obj, {$self->_config->{password_field} => $password})) {
513d8ab6 59 $c->set_authenticated($user_obj);
60 return $user_obj;
d99b7693 61 }
62 }
513d8ab6 63 else {
64 $c->log->debug("Unable to locate user matching user info provided") if $c->debug;
65 return;
66 }
d99b7693 67 }
68
513d8ab6 69 return;
d99b7693 70}
71
72sub authenticate_digest {
513d8ab6 73 my ( $self, $c, $realm, $auth_info ) = @_;
d99b7693 74
75 $c->log->debug('Checking http digest authentication.') if $c->debug;
76
77 my $headers = $c->req->headers;
78 my @authorization = $headers->header('Authorization');
79 foreach my $authorization (@authorization) {
80 next unless $authorization =~ m{^Digest};
d99b7693 81 my %res = map {
82 my @key_val = split /=/, $_, 2;
83 $key_val[0] = lc $key_val[0];
84 $key_val[1] =~ s{"}{}g; # remove the quotes
85 @key_val;
86 } split /,\s?/, substr( $authorization, 7 ); #7 == length "Digest "
87
88 my $opaque = $res{opaque};
513d8ab6 89 my $nonce = $self->get_digest_authorization_nonce( $c, __PACKAGE__ . '::opaque:' . $opaque );
d99b7693 90 next unless $nonce;
91
92 $c->log->debug('Checking authentication parameters.')
93 if $c->debug;
94
95 my $uri = '/' . $c->request->path;
96 my $algorithm = $res{algorithm} || 'MD5';
97 my $nonce_count = '0x' . $res{nc};
98
99 my $check = $uri eq $res{uri}
100 && ( exists $res{username} )
101 && ( exists $res{qop} )
102 && ( exists $res{cnonce} )
103 && ( exists $res{nc} )
104 && $algorithm eq $nonce->algorithm
105 && hex($nonce_count) > hex( $nonce->nonce_count )
106 && $res{nonce} eq $nonce->nonce; # TODO: set Stale instead
107
108 unless ($check) {
109 $c->log->debug('Digest authentication failed. Bad request.')
110 if $c->debug;
111 $c->res->status(400); # bad request
513d8ab6 112 Carp::confess $Catalyst::DETACH;
d99b7693 113 }
114
115 $c->log->debug('Checking authentication response.')
116 if $c->debug;
117
118 my $username = $res{username};
d99b7693 119
120 my $user;
121
513d8ab6 122 unless ( $user = $auth_info->{user} ) {
123 $user = $realm->find_user( { username => $username }, $c);
d99b7693 124 }
d99b7693 125 unless ($user) { # no user, no authentication
513d8ab6 126 $c->log->debug("Unable to locate user matching user info provided") if $c->debug;
127 return;
d99b7693 128 }
129
130 # everything looks good, let's check the response
d99b7693 131 # calculate H(A2) as per spec
132 my $ctx = Digest::MD5->new;
133 $ctx->add( join( ':', $c->request->method, $res{uri} ) );
134 if ( $res{qop} eq 'auth-int' ) {
135 my $digest =
136 Digest::MD5::md5_hex( $c->request->body ); # not sure here
137 $ctx->add( ':', $digest );
138 }
139 my $A2_digest = $ctx->hexdigest;
140
141 # the idea of the for loop:
142 # if we do not want to store the plain password in our user store,
143 # we can store md5_hex("$username:$realm:$password") instead
490754a8 144 my $password_field = $self->_config->{password_field};
d99b7693 145 for my $r ( 0 .. 1 ) {
490754a8 146 # FIXME - Do not assume accessor is called password.
d99b7693 147 # calculate H(A1) as per spec
490754a8 148 my $A1_digest = $r ? $user->$password_field() : do {
d99b7693 149 $ctx = Digest::MD5->new;
490754a8 150 $ctx->add( join( ':', $username, $realm->name, $user->$password_field() ) );
d99b7693 151 $ctx->hexdigest;
152 };
153 if ( $nonce->algorithm eq 'MD5-sess' ) {
154 $ctx = Digest::MD5->new;
155 $ctx->add( join( ':', $A1_digest, $res{nonce}, $res{cnonce} ) );
156 $A1_digest = $ctx->hexdigest;
157 }
158
513d8ab6 159 my $digest_in = join( ':',
d99b7693 160 $A1_digest, $res{nonce},
161 $res{qop} ? ( $res{nc}, $res{cnonce}, $res{qop} ) : (),
513d8ab6 162 $A2_digest );
163 my $rq_digest = Digest::MD5::md5_hex($digest_in);
d99b7693 164 $nonce->nonce_count($nonce_count);
165 $c->cache->set( __PACKAGE__ . '::opaque:' . $nonce->opaque,
166 $nonce );
513d8ab6 167 if ($rq_digest eq $res{response}) {
168 $c->set_authenticated($user);
169 return 1;
170 }
d99b7693 171 }
172 }
513d8ab6 173 return;
d99b7693 174}
175
176sub _check_cache {
177 my $c = shift;
178
179 die "A cache is needed for http digest authentication."
180 unless $c->can('cache');
513d8ab6 181 return;
d99b7693 182}
183
184sub _is_http_auth_type {
513d8ab6 185 my ( $self, $type ) = @_;
186 my $cfgtype = lc( $self->_config->{'type'} || 'any' );
d99b7693 187 return 1 if $cfgtype eq 'any' || $cfgtype eq lc $type;
188 return 0;
189}
190
d99b7693 191sub authorization_required_response {
513d8ab6 192 my ( $self, $c, $realm, $auth_info ) = @_;
d99b7693 193
194 $c->res->status(401);
195 $c->res->content_type('text/plain');
513d8ab6 196 if (exists $self->_config->{authorization_required_message}) {
197 # If you set the key to undef, don't stamp on the body.
198 $c->res->body($self->_config->{authorization_required_message})
199 if defined $c->res->body($self->_config->{authorization_required_message});
200 }
201 else {
202 $c->res->body('Authorization required.');
203 }
d99b7693 204
205 # *DONT* short circuit
206 my $ok;
513d8ab6 207 $ok++ if $self->_create_digest_auth_response($c, $auth_info);
208 $ok++ if $self->_create_basic_auth_response($c, $auth_info);
d99b7693 209
210 unless ( $ok ) {
211 die 'Could not build authorization required response. '
212 . 'Did you configure a valid authentication http type: '
213 . 'basic, digest, any';
214 }
513d8ab6 215 return;
d99b7693 216}
217
218sub _add_authentication_header {
219 my ( $c, $header ) = @_;
513d8ab6 220 $c->response->headers->push_header( 'WWW-Authenticate' => $header );
221 return;
d99b7693 222}
223
224sub _create_digest_auth_response {
513d8ab6 225 my ( $self, $c, $opts ) = @_;
d99b7693 226
513d8ab6 227 return unless $self->_is_http_auth_type('digest');
d99b7693 228
513d8ab6 229 if ( my $digest = $self->_build_digest_auth_header( $c, $opts ) ) {
230 _add_authentication_header( $c, $digest );
d99b7693 231 return 1;
232 }
233
234 return;
235}
236
237sub _create_basic_auth_response {
513d8ab6 238 my ( $self, $c, $opts ) = @_;
d99b7693 239
513d8ab6 240 return unless $self->_is_http_auth_type('basic');
d99b7693 241
513d8ab6 242 if ( my $basic = $self->_build_basic_auth_header( $c, $opts ) ) {
243 _add_authentication_header( $c, $basic );
d99b7693 244 return 1;
245 }
246
247 return;
248}
249
250sub _build_auth_header_realm {
513d8ab6 251 my ( $self ) = @_;
252
253 if ( my $realm = $self->realm ) {
254 my $realm_name = String::Escape::qprintable($realm->name);
255 $realm_name = qq{"$realm_name"} unless $realm_name =~ /^"/;
256 return 'realm=' . $realm_name;
257 }
258 return;
d99b7693 259}
260
261sub _build_auth_header_domain {
513d8ab6 262 my ( $self, $c, $opts ) = @_;
d99b7693 263
264 if ( my $domain = $opts->{domain} ) {
265 Catalyst::Exception->throw("domain must be an array reference")
266 unless ref($domain) && ref($domain) eq "ARRAY";
267
268 my @uris =
513d8ab6 269 $self->_config->{use_uri_for}
d99b7693 270 ? ( map { $c->uri_for($_) } @$domain )
271 : ( map { URI::Escape::uri_escape($_) } @$domain );
272
273 return qq{domain="@uris"};
513d8ab6 274 }
275 return;
d99b7693 276}
277
278sub _build_auth_header_common {
513d8ab6 279 my ( $self, $c, $opts ) = @_;
d99b7693 280
281 return (
513d8ab6 282 $self->_build_auth_header_realm(),
283 $self->_build_auth_header_domain($c, $opts),
d99b7693 284 );
285}
286
287sub _build_basic_auth_header {
513d8ab6 288 my ( $self, $c, $opts ) = @_;
289 return _join_auth_header_parts( Basic => $self->_build_auth_header_common( $c, $opts ) );
d99b7693 290}
291
292sub _build_digest_auth_header {
513d8ab6 293 my ( $self, $c, $opts ) = @_;
d99b7693 294
513d8ab6 295 my $nonce = $self->_digest_auth_nonce($c, $opts);
d99b7693 296
297 my $key = __PACKAGE__ . '::opaque:' . $nonce->opaque;
298
513d8ab6 299 $self->store_digest_authorization_nonce( $c, $key, $nonce );
a14203f8 300
513d8ab6 301 return _join_auth_header_parts( Digest =>
302 $self->_build_auth_header_common($c, $opts),
d99b7693 303 map { sprintf '%s="%s"', $_, $nonce->$_ } qw(
304 qop
305 nonce
306 opaque
307 algorithm
308 ),
309 );
310}
a14203f8 311
d99b7693 312sub _digest_auth_nonce {
513d8ab6 313 my ( $self, $c, $opts ) = @_;
d99b7693 314
315 my $package = __PACKAGE__ . '::Nonce';
316
317 my $nonce = $package->new;
318
513d8ab6 319 if ( my $algorithm = $opts->{algorithm} || $self->_config->{algorithm}) {
d99b7693 320 $nonce->algorithm( $algorithm );
321 }
322
323 return $nonce;
324}
325
326sub _join_auth_header_parts {
513d8ab6 327 my ( $type, @parts ) = @_;
d99b7693 328 return "$type " . join(", ", @parts );
329}
330
331sub get_digest_authorization_nonce {
513d8ab6 332 my ( $self, $c, $key ) = @_;
333
334 _check_cache($c);
335 return $c->cache->get( $key );
d99b7693 336}
337
338sub store_digest_authorization_nonce {
513d8ab6 339 my ( $self, $c, $key, $nonce ) = @_;
d99b7693 340
513d8ab6 341 _check_cache($c);
342 return $c->cache->set( $key, $nonce );
d99b7693 343}
344
513d8ab6 345package Catalyst::Authentication::Credential::HTTP::Nonce;
d99b7693 346
347use strict;
348use base qw[ Class::Accessor::Fast ];
349use Data::UUID ();
350
513d8ab6 351our $VERSION = '0.02';
d99b7693 352
353__PACKAGE__->mk_accessors(qw[ nonce nonce_count qop opaque algorithm ]);
354
355sub new {
356 my $class = shift;
357 my $self = $class->SUPER::new(@_);
358
359 $self->nonce( Data::UUID->new->create_b64 );
360 $self->opaque( Data::UUID->new->create_b64 );
361 $self->qop('auth,auth-int');
362 $self->nonce_count('0x0');
363 $self->algorithm('MD5');
364
365 return $self;
366}
a14203f8 367
a14203f8 3681;
369
a14203f8 370__END__
371
a14203f8 372=pod
373
a14203f8 374=head1 NAME
375
513d8ab6 376Catalyst::Authentication::Credential::HTTP - HTTP Basic and Digest authentication
53306b93 377for Catalyst.
a14203f8 378
a14203f8 379=head1 SYNOPSIS
380
a14203f8 381 use Catalyst qw/
a14203f8 382 Authentication
a14203f8 383 /;
384
513d8ab6 385 __PACKAGE__->config( authentication => {
386 realms => {
387 example => {
388 credential => {
389 class => 'HTTP',
390 type => 'any', # or 'digest' or 'basic'
490754a8 391 password_type => 'clear',
392 password_field => 'password'
513d8ab6 393 },
394 store => {
395 class => 'Minimal',
396 users => {
397 Mufasa => { password => "Circle Of Life", },
398 },
399 },
400 },
401 }
402 });
d99b7693 403
404 sub foo : Local {
405 my ( $self, $c ) = @_;
406
513d8ab6 407 $c->authenticate({ realm => "example" });
d99b7693 408 # either user gets authenticated or 401 is sent
409
410 do_stuff();
411 }
412
413 # with ACL plugin
513d8ab6 414 __PACKAGE__->deny_access_unless("/path", sub { $_[0]->authenticate });
d99b7693 415
a14203f8 416=head1 DESCRIPTION
417
513d8ab6 418This module lets you use HTTP authentication with
d99b7693 419L<Catalyst::Plugin::Authentication>. Both basic and digest authentication
420are currently supported.
421
422When authentication is required, this module sets a status of 401, and
423the body of the response to 'Authorization required.'. To override
424this and set your own content, check for the C<< $c->res->status ==
425401 >> in your C<end> action, and change the body accordingly.
426
427=head2 TERMS
428
429=over 4
430
431=item Nonce
432
433A nonce is a one-time value sent with each digest authentication
434request header. The value must always be unique, so per default the
435last value of the nonce is kept using L<Catalyst::Plugin::Cache>. To
436change this behaviour, override the
437C<store_digest_authorization_nonce> and
438C<get_digest_authorization_nonce> methods as shown below.
439
440=back
441
442=head1 METHODS
443
444=over 4
445
513d8ab6 446=item new $config, $c, $realm
d99b7693 447
513d8ab6 448Simple constructor.
d99b7693 449
513d8ab6 450=item authenticate $c, $realm, \%auth_info
d99b7693 451
513d8ab6 452Tries to authenticate the user, and if that fails calls
453C<authorization_required_response> and detaches the current action call stack.
d99b7693 454
455Looks inside C<< $c->request->headers >> and processes the digest and basic
456(badly named) authorization header.
457
458This will only try the methods set in the configuration. First digest, then basic.
459
513d8ab6 460This method just passes the options through untouched. See the next two methods for what \%auth_info can contain.
d99b7693 461
513d8ab6 462=item authenticate_basic $c, $realm, \%auth_info
d99b7693 463
490754a8 464Acts like L<Catalyst::Authentication::Credential::Password>, and will lookup the user's password as detailed in that module.
465
513d8ab6 466=item authenticate_digest $c, $realm, \%auth_info
d99b7693 467
490754a8 468Assumes that your user object has a hard coded method which returns a clear text password.
d99b7693 469
513d8ab6 470=item authorization_required_response $c, $realm, \%auth_info
d99b7693 471
472Sets C<< $c->response >> to the correct status code, and adds the correct
473header to demand authentication data from the user agent.
474
513d8ab6 475Typically used by C<authenticate>, but may be invoked manually.
d99b7693 476
513d8ab6 477%opts can contain C<domain> and C<algorithm>, which are used to build
d99b7693 478%the digest header.
479
513d8ab6 480=item store_digest_authorization_nonce $c, $key, $nonce
d99b7693 481
513d8ab6 482=item get_digest_authorization_nonce $c, $key
d99b7693 483
484Set or get the C<$nonce> object used by the digest auth mode.
485
486You may override these methods. By default they will call C<get> and C<set> on
487C<< $c->cache >>.
488
d99b7693 489=back
490
491=head1 CONFIGURATION
492
513d8ab6 493All configuration is stored in C<< YourApp->config(authentication => { yourrealm => { credential => { class => 'HTTP', %config } } } >>.
d99b7693 494
495This should be a hash, and it can contain the following entries:
496
497=over 4
498
d99b7693 499=item type
500
501Can be either C<any> (the default), C<basic> or C<digest>.
502
513d8ab6 503This controls C<authorization_required_response> and C<authenticate>, but
d99b7693 504not the "manual" methods.
505
506=item authorization_required_message
507
513d8ab6 508Set this to a string to override the default body content "Authorization required.", or set to undef to suppress body content being generated.
d99b7693 509
510=back
511
512=head1 RESTRICTIONS
513
514When using digest authentication, this module will only work together
515with authentication stores whose User objects have a C<password>
516method that returns the plain-text password. It will not work together
517with L<Catalyst::Authentication::Store::Htpasswd>, or
513d8ab6 518L<Catalyst::Authentication::Store::DBIC> stores whose
d99b7693 519C<password> methods return a hashed or salted version of the password.
c7b3e379 520
a14203f8 521=head1 AUTHORS
522
513d8ab6 523Updated to current name space and currently maintained
524by: Tomas Doran C<bobtfish@bobtfish.net>.
525
526Original module by:
527
528=over
a14203f8 529
513d8ab6 530=item Yuval Kogman, C<nothingmuch@woobling.org>
a14203f8 531
513d8ab6 532=item Jess Robinson
533
534=item Sascha Kiefer C<esskar@cpan.org>
535
536=back
a14203f8 537
c7b3e379 538=head1 SEE ALSO
539
d99b7693 540RFC 2617 (or its successors), L<Catalyst::Plugin::Cache>, L<Catalyst::Plugin::Authentication>
c7b3e379 541
a14203f8 542=head1 COPYRIGHT & LICENSE
543
513d8ab6 544 Copyright (c) 2005-2008 the aforementioned authors. All rights
a14203f8 545 reserved. This program is free software; you can redistribute
a14203f8 546 it and/or modify it under the same terms as Perl itself.
547
a14203f8 548=cut
513d8ab6 549