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