Pour all my changes for the new auth framework on top of the module.
[catagits/Catalyst-Authentication-Credential-HTTP.git] / lib / Catalyst / Authentication / Credential / HTTP.pm
CommitLineData
513d8ab6 1package Catalyst::Authentication::Credential::HTTP;
2use base qw/Catalyst::Component/;
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
513d8ab6 16our $VERSION = "1.000";
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)) {
58 if ($user_obj->check_password($password)) {
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
144 for my $r ( 0 .. 1 ) {
145
146 # calculate H(A1) as per spec
147 my $A1_digest = $r ? $user->password : do {
148 $ctx = Digest::MD5->new;
513d8ab6 149 $ctx->add( join( ':', $username, $realm->name, $user->password ) );
d99b7693 150 $ctx->hexdigest;
151 };
152 if ( $nonce->algorithm eq 'MD5-sess' ) {
153 $ctx = Digest::MD5->new;
154 $ctx->add( join( ':', $A1_digest, $res{nonce}, $res{cnonce} ) );
155 $A1_digest = $ctx->hexdigest;
156 }
157
513d8ab6 158 my $digest_in = join( ':',
d99b7693 159 $A1_digest, $res{nonce},
160 $res{qop} ? ( $res{nc}, $res{cnonce}, $res{qop} ) : (),
513d8ab6 161 $A2_digest );
162 my $rq_digest = Digest::MD5::md5_hex($digest_in);
d99b7693 163 $nonce->nonce_count($nonce_count);
164 $c->cache->set( __PACKAGE__ . '::opaque:' . $nonce->opaque,
165 $nonce );
513d8ab6 166 if ($rq_digest eq $res{response}) {
167 $c->set_authenticated($user);
168 return 1;
169 }
d99b7693 170 }
171 }
513d8ab6 172 return;
d99b7693 173}
174
175sub _check_cache {
176 my $c = shift;
177
178 die "A cache is needed for http digest authentication."
179 unless $c->can('cache');
513d8ab6 180 return;
d99b7693 181}
182
183sub _is_http_auth_type {
513d8ab6 184 my ( $self, $type ) = @_;
185 my $cfgtype = lc( $self->_config->{'type'} || 'any' );
d99b7693 186 return 1 if $cfgtype eq 'any' || $cfgtype eq lc $type;
187 return 0;
188}
189
d99b7693 190sub authorization_required_response {
513d8ab6 191 my ( $self, $c, $realm, $auth_info ) = @_;
d99b7693 192
193 $c->res->status(401);
194 $c->res->content_type('text/plain');
513d8ab6 195 if (exists $self->_config->{authorization_required_message}) {
196 # If you set the key to undef, don't stamp on the body.
197 $c->res->body($self->_config->{authorization_required_message})
198 if defined $c->res->body($self->_config->{authorization_required_message});
199 }
200 else {
201 $c->res->body('Authorization required.');
202 }
d99b7693 203
204 # *DONT* short circuit
205 my $ok;
513d8ab6 206 $ok++ if $self->_create_digest_auth_response($c, $auth_info);
207 $ok++ if $self->_create_basic_auth_response($c, $auth_info);
d99b7693 208
209 unless ( $ok ) {
210 die 'Could not build authorization required response. '
211 . 'Did you configure a valid authentication http type: '
212 . 'basic, digest, any';
213 }
513d8ab6 214 return;
d99b7693 215}
216
217sub _add_authentication_header {
218 my ( $c, $header ) = @_;
513d8ab6 219 $c->response->headers->push_header( 'WWW-Authenticate' => $header );
220 return;
d99b7693 221}
222
223sub _create_digest_auth_response {
513d8ab6 224 my ( $self, $c, $opts ) = @_;
d99b7693 225
513d8ab6 226 return unless $self->_is_http_auth_type('digest');
d99b7693 227
513d8ab6 228 if ( my $digest = $self->_build_digest_auth_header( $c, $opts ) ) {
229 _add_authentication_header( $c, $digest );
d99b7693 230 return 1;
231 }
232
233 return;
234}
235
236sub _create_basic_auth_response {
513d8ab6 237 my ( $self, $c, $opts ) = @_;
d99b7693 238
513d8ab6 239 return unless $self->_is_http_auth_type('basic');
d99b7693 240
513d8ab6 241 if ( my $basic = $self->_build_basic_auth_header( $c, $opts ) ) {
242 _add_authentication_header( $c, $basic );
d99b7693 243 return 1;
244 }
245
246 return;
247}
248
249sub _build_auth_header_realm {
513d8ab6 250 my ( $self ) = @_;
251
252 if ( my $realm = $self->realm ) {
253 my $realm_name = String::Escape::qprintable($realm->name);
254 $realm_name = qq{"$realm_name"} unless $realm_name =~ /^"/;
255 return 'realm=' . $realm_name;
256 }
257 return;
d99b7693 258}
259
260sub _build_auth_header_domain {
513d8ab6 261 my ( $self, $c, $opts ) = @_;
d99b7693 262
263 if ( my $domain = $opts->{domain} ) {
264 Catalyst::Exception->throw("domain must be an array reference")
265 unless ref($domain) && ref($domain) eq "ARRAY";
266
267 my @uris =
513d8ab6 268 $self->_config->{use_uri_for}
d99b7693 269 ? ( map { $c->uri_for($_) } @$domain )
270 : ( map { URI::Escape::uri_escape($_) } @$domain );
271
272 return qq{domain="@uris"};
513d8ab6 273 }
274 return;
d99b7693 275}
276
277sub _build_auth_header_common {
513d8ab6 278 my ( $self, $c, $opts ) = @_;
d99b7693 279
280 return (
513d8ab6 281 $self->_build_auth_header_realm(),
282 $self->_build_auth_header_domain($c, $opts),
d99b7693 283 );
284}
285
286sub _build_basic_auth_header {
513d8ab6 287 my ( $self, $c, $opts ) = @_;
288 return _join_auth_header_parts( Basic => $self->_build_auth_header_common( $c, $opts ) );
d99b7693 289}
290
291sub _build_digest_auth_header {
513d8ab6 292 my ( $self, $c, $opts ) = @_;
d99b7693 293
513d8ab6 294 my $nonce = $self->_digest_auth_nonce($c, $opts);
d99b7693 295
296 my $key = __PACKAGE__ . '::opaque:' . $nonce->opaque;
297
513d8ab6 298 $self->store_digest_authorization_nonce( $c, $key, $nonce );
a14203f8 299
513d8ab6 300 return _join_auth_header_parts( Digest =>
301 $self->_build_auth_header_common($c, $opts),
d99b7693 302 map { sprintf '%s="%s"', $_, $nonce->$_ } qw(
303 qop
304 nonce
305 opaque
306 algorithm
307 ),
308 );
309}
a14203f8 310
d99b7693 311sub _digest_auth_nonce {
513d8ab6 312 my ( $self, $c, $opts ) = @_;
d99b7693 313
314 my $package = __PACKAGE__ . '::Nonce';
315
316 my $nonce = $package->new;
317
513d8ab6 318 if ( my $algorithm = $opts->{algorithm} || $self->_config->{algorithm}) {
d99b7693 319 $nonce->algorithm( $algorithm );
320 }
321
322 return $nonce;
323}
324
325sub _join_auth_header_parts {
513d8ab6 326 my ( $type, @parts ) = @_;
d99b7693 327 return "$type " . join(", ", @parts );
328}
329
330sub get_digest_authorization_nonce {
513d8ab6 331 my ( $self, $c, $key ) = @_;
332
333 _check_cache($c);
334 return $c->cache->get( $key );
d99b7693 335}
336
337sub store_digest_authorization_nonce {
513d8ab6 338 my ( $self, $c, $key, $nonce ) = @_;
d99b7693 339
513d8ab6 340 _check_cache($c);
341 return $c->cache->set( $key, $nonce );
d99b7693 342}
343
513d8ab6 344package Catalyst::Authentication::Credential::HTTP::Nonce;
d99b7693 345
346use strict;
347use base qw[ Class::Accessor::Fast ];
348use Data::UUID ();
349
513d8ab6 350our $VERSION = '0.02';
d99b7693 351
352__PACKAGE__->mk_accessors(qw[ nonce nonce_count qop opaque algorithm ]);
353
354sub new {
355 my $class = shift;
356 my $self = $class->SUPER::new(@_);
357
358 $self->nonce( Data::UUID->new->create_b64 );
359 $self->opaque( Data::UUID->new->create_b64 );
360 $self->qop('auth,auth-int');
361 $self->nonce_count('0x0');
362 $self->algorithm('MD5');
363
364 return $self;
365}
a14203f8 366
a14203f8 3671;
368
a14203f8 369__END__
370
a14203f8 371=pod
372
a14203f8 373=head1 NAME
374
513d8ab6 375Catalyst::Authentication::Credential::HTTP - HTTP Basic and Digest authentication
53306b93 376for Catalyst.
a14203f8 377
a14203f8 378=head1 SYNOPSIS
379
a14203f8 380 use Catalyst qw/
a14203f8 381 Authentication
a14203f8 382 /;
383
513d8ab6 384 __PACKAGE__->config( authentication => {
385 realms => {
386 example => {
387 credential => {
388 class => 'HTTP',
389 type => 'any', # or 'digest' or 'basic'
390 },
391 store => {
392 class => 'Minimal',
393 users => {
394 Mufasa => { password => "Circle Of Life", },
395 },
396 },
397 },
398 }
399 });
d99b7693 400
401 sub foo : Local {
402 my ( $self, $c ) = @_;
403
513d8ab6 404 $c->authenticate({ realm => "example" });
d99b7693 405 # either user gets authenticated or 401 is sent
406
407 do_stuff();
408 }
409
410 # with ACL plugin
513d8ab6 411 __PACKAGE__->deny_access_unless("/path", sub { $_[0]->authenticate });
d99b7693 412
a14203f8 413=head1 DESCRIPTION
414
513d8ab6 415This module lets you use HTTP authentication with
d99b7693 416L<Catalyst::Plugin::Authentication>. Both basic and digest authentication
417are currently supported.
418
419When authentication is required, this module sets a status of 401, and
420the body of the response to 'Authorization required.'. To override
421this and set your own content, check for the C<< $c->res->status ==
422401 >> in your C<end> action, and change the body accordingly.
423
424=head2 TERMS
425
426=over 4
427
428=item Nonce
429
430A nonce is a one-time value sent with each digest authentication
431request header. The value must always be unique, so per default the
432last value of the nonce is kept using L<Catalyst::Plugin::Cache>. To
433change this behaviour, override the
434C<store_digest_authorization_nonce> and
435C<get_digest_authorization_nonce> methods as shown below.
436
437=back
438
439=head1 METHODS
440
441=over 4
442
513d8ab6 443=item new $config, $c, $realm
d99b7693 444
513d8ab6 445Simple constructor.
d99b7693 446
513d8ab6 447=item authenticate $c, $realm, \%auth_info
d99b7693 448
513d8ab6 449Tries to authenticate the user, and if that fails calls
450C<authorization_required_response> and detaches the current action call stack.
d99b7693 451
452Looks inside C<< $c->request->headers >> and processes the digest and basic
453(badly named) authorization header.
454
455This will only try the methods set in the configuration. First digest, then basic.
456
513d8ab6 457This method just passes the options through untouched. See the next two methods for what \%auth_info can contain.
d99b7693 458
513d8ab6 459=item authenticate_basic $c, $realm, \%auth_info
d99b7693 460
513d8ab6 461=item authenticate_digest $c, $realm, \%auth_info
d99b7693 462
463Try to authenticate one of the methods without checking if the method is
464allowed in the configuration.
465
513d8ab6 466=item authorization_required_response $c, $realm, \%auth_info
d99b7693 467
468Sets C<< $c->response >> to the correct status code, and adds the correct
469header to demand authentication data from the user agent.
470
513d8ab6 471Typically used by C<authenticate>, but may be invoked manually.
d99b7693 472
513d8ab6 473%opts can contain C<domain> and C<algorithm>, which are used to build
d99b7693 474%the digest header.
475
513d8ab6 476=item store_digest_authorization_nonce $c, $key, $nonce
d99b7693 477
513d8ab6 478=item get_digest_authorization_nonce $c, $key
d99b7693 479
480Set or get the C<$nonce> object used by the digest auth mode.
481
482You may override these methods. By default they will call C<get> and C<set> on
483C<< $c->cache >>.
484
d99b7693 485=back
486
487=head1 CONFIGURATION
488
513d8ab6 489All configuration is stored in C<< YourApp->config(authentication => { yourrealm => { credential => { class => 'HTTP', %config } } } >>.
d99b7693 490
491This should be a hash, and it can contain the following entries:
492
493=over 4
494
d99b7693 495=item type
496
497Can be either C<any> (the default), C<basic> or C<digest>.
498
513d8ab6 499This controls C<authorization_required_response> and C<authenticate>, but
d99b7693 500not the "manual" methods.
501
502=item authorization_required_message
503
513d8ab6 504Set this to a string to override the default body content "Authorization required.", or set to undef to suppress body content being generated.
d99b7693 505
506=back
507
508=head1 RESTRICTIONS
509
510When using digest authentication, this module will only work together
511with authentication stores whose User objects have a C<password>
512method that returns the plain-text password. It will not work together
513with L<Catalyst::Authentication::Store::Htpasswd>, or
513d8ab6 514L<Catalyst::Authentication::Store::DBIC> stores whose
d99b7693 515C<password> methods return a hashed or salted version of the password.
c7b3e379 516
a14203f8 517=head1 AUTHORS
518
513d8ab6 519Updated to current name space and currently maintained
520by: Tomas Doran C<bobtfish@bobtfish.net>.
521
522Original module by:
523
524=over
a14203f8 525
513d8ab6 526=item Yuval Kogman, C<nothingmuch@woobling.org>
a14203f8 527
513d8ab6 528=item Jess Robinson
529
530=item Sascha Kiefer C<esskar@cpan.org>
531
532=back
a14203f8 533
c7b3e379 534=head1 SEE ALSO
535
d99b7693 536RFC 2617 (or its successors), L<Catalyst::Plugin::Cache>, L<Catalyst::Plugin::Authentication>
c7b3e379 537
a14203f8 538=head1 COPYRIGHT & LICENSE
539
513d8ab6 540 Copyright (c) 2005-2008 the aforementioned authors. All rights
a14203f8 541 reserved. This program is free software; you can redistribute
a14203f8 542 it and/or modify it under the same terms as Perl itself.
543
a14203f8 544=cut
513d8ab6 545