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