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