Add an extra field which I need
[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 => {
387 realms => {
388 example => {
389 credential => {
390 class => 'HTTP',
391 type => 'any', # or 'digest' or 'basic'
490754a8 392 password_type => 'clear',
393 password_field => 'password'
513d8ab6 394 },
395 store => {
396 class => 'Minimal',
397 users => {
398 Mufasa => { password => "Circle Of Life", },
399 },
400 },
401 },
402 }
403 });
d99b7693 404
405 sub foo : Local {
406 my ( $self, $c ) = @_;
407
513d8ab6 408 $c->authenticate({ realm => "example" });
d99b7693 409 # either user gets authenticated or 401 is sent
bf399285 410 # Note that the authentication realm sent to the client is overridden
411 # here, but this does not affect the Catalyst::Authentication::Realm
412 # used for authentication.
d99b7693 413
414 do_stuff();
415 }
031f556c 416
417 sub always_auth : Local {
418 my ( $self, $c ) = @_;
419
420 # Force authorization headers onto the response so that the user
421 # is asked again for authentication, even if they successfully
422 # authenticated.
423 my $realm = $c->get_auth_realm('example');
424 $realm->credential->authorization_required_response($c, $realm);
425 }
d99b7693 426
427 # with ACL plugin
513d8ab6 428 __PACKAGE__->deny_access_unless("/path", sub { $_[0]->authenticate });
d99b7693 429
a14203f8 430=head1 DESCRIPTION
431
513d8ab6 432This module lets you use HTTP authentication with
d99b7693 433L<Catalyst::Plugin::Authentication>. Both basic and digest authentication
434are currently supported.
435
436When authentication is required, this module sets a status of 401, and
437the body of the response to 'Authorization required.'. To override
438this and set your own content, check for the C<< $c->res->status ==
439401 >> in your C<end> action, and change the body accordingly.
440
441=head2 TERMS
442
443=over 4
444
445=item Nonce
446
447A nonce is a one-time value sent with each digest authentication
448request header. The value must always be unique, so per default the
449last value of the nonce is kept using L<Catalyst::Plugin::Cache>. To
450change this behaviour, override the
451C<store_digest_authorization_nonce> and
452C<get_digest_authorization_nonce> methods as shown below.
453
454=back
455
456=head1 METHODS
457
458=over 4
459
513d8ab6 460=item new $config, $c, $realm
d99b7693 461
513d8ab6 462Simple constructor.
d99b7693 463
41091cd6 464=item init
465
466Validates that $config is ok.
467
513d8ab6 468=item authenticate $c, $realm, \%auth_info
d99b7693 469
513d8ab6 470Tries to authenticate the user, and if that fails calls
471C<authorization_required_response> and detaches the current action call stack.
d99b7693 472
473Looks inside C<< $c->request->headers >> and processes the digest and basic
474(badly named) authorization header.
475
476This will only try the methods set in the configuration. First digest, then basic.
477
bf399285 478The %auth_info hash can contain a number of keys which control the authentication behaviour:
479
480=over
481
482=item realm
483
484Sets the HTTP authentication realm presented to the client. Note this does not alter the
485Catalyst::Authentication::Realm object used for the authentication.
486
05512a69 487=item domain
bf399285 488
05512a69 489Array reference to domains used to build the authorization headers.
bf399285 490
ea92acf7 491This list of domains defines the protection space. If a domain URI is an
492absolute path (starts with /), it is relative to the root URL of the server being accessed.
493An absolute URI in this list may refer to a different server than the one being accessed.
494
495The client will use this list to determine the set of URIs for which the same authentication
496information may be sent.
497
498If this is omitted or its value is empty, the client will assume that the
499protection space consists of all URIs on the responding server.
500
501Therefore, if your application is not hosted at the root of this domain, and you want to
502prevent the authentication credentials for this application being sent to any other applications.
503then you should use the I<use_uri_for> configuration option, and pass a domain of I</>.
504
bf399285 505=back
d99b7693 506
513d8ab6 507=item authenticate_basic $c, $realm, \%auth_info
d99b7693 508
bf399285 509Performs HTTP basic authentication.
490754a8 510
513d8ab6 511=item authenticate_digest $c, $realm, \%auth_info
d99b7693 512
bf399285 513Performs HTTP digest authentication. Note that the password_type B<must> by I<clear> for
c5a1fa88 514digest authentication to succeed, and you must have L<Catalyst::Plugin::Session> in
515your application as digest authentication needs to store persistent data.
516
517Note - if you do not want to store your user passwords as clear text, then it is possible
518to store instead the MD5 digest in hex of the string '$username:$realm:$password'
d99b7693 519
ea92acf7 520Takes an additional parameter of I<algorithm>, the possible values of which are 'MD5' (the default)
521and 'MD5-sess'. For more information about 'MD5-sess', see section 3.2.2.2 in RFC 2617.
522
513d8ab6 523=item authorization_required_response $c, $realm, \%auth_info
d99b7693 524
525Sets C<< $c->response >> to the correct status code, and adds the correct
526header to demand authentication data from the user agent.
527
513d8ab6 528Typically used by C<authenticate>, but may be invoked manually.
d99b7693 529
513d8ab6 530%opts can contain C<domain> and C<algorithm>, which are used to build
d99b7693 531%the digest header.
532
513d8ab6 533=item store_digest_authorization_nonce $c, $key, $nonce
d99b7693 534
513d8ab6 535=item get_digest_authorization_nonce $c, $key
d99b7693 536
537Set or get the C<$nonce> object used by the digest auth mode.
538
539You may override these methods. By default they will call C<get> and C<set> on
540C<< $c->cache >>.
541
d99b7693 542=back
543
544=head1 CONFIGURATION
545
513d8ab6 546All configuration is stored in C<< YourApp->config(authentication => { yourrealm => { credential => { class => 'HTTP', %config } } } >>.
d99b7693 547
548This should be a hash, and it can contain the following entries:
549
05512a69 550=over
d99b7693 551
d99b7693 552=item type
553
554Can be either C<any> (the default), C<basic> or C<digest>.
555
513d8ab6 556This controls C<authorization_required_response> and C<authenticate>, but
d99b7693 557not the "manual" methods.
558
559=item authorization_required_message
560
513d8ab6 561Set this to a string to override the default body content "Authorization required.", or set to undef to suppress body content being generated.
d99b7693 562
05512a69 563=item password_type
564
f1f73b53 565The type of password returned by the user object. Same usage as in
b56120cd 566L<Catalyst::Authentication::Credential::Password|Catalyst::Authentication::Credential::Password/password_type>
05512a69 567
568=item password_field
569
f1f73b53 570The name of accessor used to retrieve the value of the password field from the user object. Same usage as in
05512a69 571L<Catalyst::Authentication::Credential::Password|Catalyst::Authentication::Credential::Password/password_field>
572
a50635bf 573=item username_field
574
575The field name that the user's username is mapped into when finding the user from the realm. Defaults to 'username'.
576
05512a69 577=item use_uri_for
578
579If this configuration key has a true value, then the domain(s) for the authorization header will be
ea92acf7 580run through $c->uri_for(). Use this configuration option if your application is not running at the root
581of your domain, and you want to ensure that authentication credentials from your application are not shared with
582other applications on the same server.
05512a69 583
d99b7693 584=back
585
586=head1 RESTRICTIONS
587
588When using digest authentication, this module will only work together
589with authentication stores whose User objects have a C<password>
590method that returns the plain-text password. It will not work together
591with L<Catalyst::Authentication::Store::Htpasswd>, or
513d8ab6 592L<Catalyst::Authentication::Store::DBIC> stores whose
d99b7693 593C<password> methods return a hashed or salted version of the password.
c7b3e379 594
a14203f8 595=head1 AUTHORS
596
513d8ab6 597Updated to current name space and currently maintained
598by: Tomas Doran C<bobtfish@bobtfish.net>.
599
600Original module by:
601
602=over
a14203f8 603
513d8ab6 604=item Yuval Kogman, C<nothingmuch@woobling.org>
a14203f8 605
513d8ab6 606=item Jess Robinson
607
608=item Sascha Kiefer C<esskar@cpan.org>
609
610=back
a14203f8 611
c7b3e379 612=head1 SEE ALSO
613
d99b7693 614RFC 2617 (or its successors), L<Catalyst::Plugin::Cache>, L<Catalyst::Plugin::Authentication>
c7b3e379 615
a14203f8 616=head1 COPYRIGHT & LICENSE
617
513d8ab6 618 Copyright (c) 2005-2008 the aforementioned authors. All rights
a14203f8 619 reserved. This program is free software; you can redistribute
a14203f8 620 it and/or modify it under the same terms as Perl itself.
621
a14203f8 622=cut
513d8ab6 623