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