Typo
[catagits/Catalyst-Authentication-Credential-HTTP.git] / lib / Catalyst / Plugin / Authentication / Credential / HTTP.pm
CommitLineData
a14203f8 1#!/usr/bin/perl
2
a14203f8 3package Catalyst::Plugin::Authentication::Credential::HTTP;
a14203f8 4use base qw/Catalyst::Plugin::Authentication::Credential::Password/;
5
a14203f8 6use strict;
a14203f8 7use warnings;
8
a14203f8 9use String::Escape ();
a14203f8 10use URI::Escape ();
a14203f8 11use Catalyst ();
a14203f8 12use Digest::MD5 ();
13
e9684b75 14our $VERSION = "0.07";
a14203f8 15
a14203f8 16sub authenticate_http {
ac92fd52 17 my ( $c, @args ) = @_;
a14203f8 18
ac92fd52 19 return 1 if $c->_is_http_auth_type('digest') && $c->authenticate_digest(@args);
20 return 1 if $c->_is_http_auth_type('basic') && $c->authenticate_basic(@args);
a14203f8 21}
22
ac92fd52 23sub get_http_auth_store {
24 my ( $c, %opts ) = @_;
371a8cc8 25
26 my $store = $opts{store} || $c->config->{authentication}{http}{store} || return;
27
28 return ref $store
29 ? $store
30 : $c->get_auth_store($store);
ac92fd52 31}
a14203f8 32
33sub authenticate_basic {
ac92fd52 34 my ( $c, %opts ) = @_;
a14203f8 35
36 $c->log->debug('Checking http basic authentication.') if $c->debug;
37
a14203f8 38 my $headers = $c->req->headers;
39
ac92fd52 40 if ( my ( $username, $password ) = $headers->authorization_basic ) {
a14203f8 41
ac92fd52 42 my $user;
a14203f8 43
ac92fd52 44 unless ( $user = $opts{user} ) {
45 if ( my $store = $c->get_http_auth_store(%opts) ) {
46 $user = $store->get_user($username);
47 } else {
48 $user = $username;
49 }
a14203f8 50 }
51
a14203f8 52 return $c->login( $user, $password );
a14203f8 53 }
54
a14203f8 55 return 0;
a14203f8 56}
57
a14203f8 58sub authenticate_digest {
ac92fd52 59 my ( $c, %opts ) = @_;
a14203f8 60
61 $c->log->debug('Checking http digest authentication.') if $c->debug;
62
a14203f8 63 my $headers = $c->req->headers;
a14203f8 64 my @authorization = $headers->header('Authorization');
a14203f8 65 foreach my $authorization (@authorization) {
a14203f8 66 next unless $authorization =~ m{^Digest};
67
a14203f8 68 my %res = map {
a14203f8 69 my @key_val = split /=/, $_, 2;
a14203f8 70 $key_val[0] = lc $key_val[0];
a14203f8 71 $key_val[1] =~ s{"}{}g; # remove the quotes
a14203f8 72 @key_val;
a14203f8 73 } split /,\s?/, substr( $authorization, 7 ); #7 == length "Digest "
74
a14203f8 75 my $opaque = $res{opaque};
371a8cc8 76 my $nonce = $c->get_digest_authorization_nonce( __PACKAGE__ . '::opaque:' . $opaque );
a14203f8 77 next unless $nonce;
78
a14203f8 79 $c->log->debug('Checking authentication parameters.')
a14203f8 80 if $c->debug;
81
a14203f8 82 my $uri = '/' . $c->request->path;
a14203f8 83 my $algorithm = $res{algorithm} || 'MD5';
a14203f8 84 my $nonce_count = '0x' . $res{nc};
85
a14203f8 86 my $check = $uri eq $res{uri}
a14203f8 87 && ( exists $res{username} )
a14203f8 88 && ( exists $res{qop} )
a14203f8 89 && ( exists $res{cnonce} )
a14203f8 90 && ( exists $res{nc} )
a14203f8 91 && $algorithm eq $nonce->algorithm
a14203f8 92 && hex($nonce_count) > hex( $nonce->nonce_count )
a14203f8 93 && $res{nonce} eq $nonce->nonce; # TODO: set Stale instead
94
a14203f8 95 unless ($check) {
a14203f8 96 $c->log->debug('Digest authentication failed. Bad request.')
a14203f8 97 if $c->debug;
a14203f8 98 $c->res->status(400); # bad request
a14203f8 99 die $Catalyst::DETACH;
a14203f8 100 }
101
a14203f8 102 $c->log->debug('Checking authentication response.')
a14203f8 103 if $c->debug;
104
a14203f8 105 my $username = $res{username};
a14203f8 106 my $realm = $res{realm};
107
a14203f8 108 my $user;
a14203f8 109
371a8cc8 110 unless ( $user = $opts{user} ) {
111 if ( my $store = $c->get_http_auth_store(%opts) || $c->default_auth_store ) {
112 $user = $store->get_user($username);
113 }
114 }
a14203f8 115
116 unless ($user) { # no user, no authentication
a14203f8 117 $c->log->debug('Unknown user: $user.') if $c->debug;
a14203f8 118 return 0;
a14203f8 119 }
120
a14203f8 121 # everything looks good, let's check the response
122
a14203f8 123 # calculate H(A2) as per spec
a14203f8 124 my $ctx = Digest::MD5->new;
a14203f8 125 $ctx->add( join( ':', $c->request->method, $res{uri} ) );
a14203f8 126 if ( $res{qop} eq 'auth-int' ) {
a14203f8 127 my $digest =
a14203f8 128 Digest::MD5::md5_hex( $c->request->body ); # not sure here
a14203f8 129 $ctx->add( ':', $digest );
a14203f8 130 }
a14203f8 131 my $A2_digest = $ctx->hexdigest;
132
a14203f8 133 # the idea of the for loop:
a14203f8 134 # if we do not want to store the plain password in our user store,
a14203f8 135 # we can store md5_hex("$username:$realm:$password") instead
a14203f8 136 for my $r ( 0 .. 1 ) {
137
a14203f8 138 # calculate H(A1) as per spec
a14203f8 139 my $A1_digest = $r ? $user->password : do {
a14203f8 140 $ctx = Digest::MD5->new;
a14203f8 141 $ctx->add( join( ':', $username, $realm, $user->password ) );
a14203f8 142 $ctx->hexdigest;
a14203f8 143 };
a14203f8 144 if ( $nonce->algorithm eq 'MD5-sess' ) {
a14203f8 145 $ctx = Digest::MD5->new;
a14203f8 146 $ctx->add( join( ':', $A1_digest, $res{nonce}, $res{cnonce} ) );
a14203f8 147 $A1_digest = $ctx->hexdigest;
a14203f8 148 }
149
a14203f8 150 my $rq_digest = Digest::MD5::md5_hex(
a14203f8 151 join( ':',
a14203f8 152 $A1_digest, $res{nonce},
a14203f8 153 $res{qop} ? ( $res{nc}, $res{cnonce}, $res{qop} ) : (),
a14203f8 154 $A2_digest )
a14203f8 155 );
156
a14203f8 157 $nonce->nonce_count($nonce_count);
a14203f8 158 $c->cache->set( __PACKAGE__ . '::opaque:' . $nonce->opaque,
a14203f8 159 $nonce );
160
a14203f8 161 return $c->login( $user, $user->password )
a14203f8 162 if $rq_digest eq $res{response};
a14203f8 163 }
a14203f8 164 }
165
a14203f8 166 return 0;
a14203f8 167}
168
a14203f8 169sub _check_cache {
a14203f8 170 my $c = shift;
171
a14203f8 172 die "A cache is needed for http digest authentication."
a14203f8 173 unless $c->can('cache');
a14203f8 174}
175
ac92fd52 176sub _is_http_auth_type {
a14203f8 177 my ( $c, $type ) = @_;
178
a14203f8 179 my $cfgtype = lc( $c->config->{authentication}{http}{type} || 'any' );
a14203f8 180 return 1 if $cfgtype eq 'any' || $cfgtype eq lc $type;
a14203f8 181 return 0;
a14203f8 182}
183
a14203f8 184sub authorization_required {
ac92fd52 185 my ( $c, @args ) = @_;
a14203f8 186
ac92fd52 187 return 1 if $c->authenticate_http(@args);
188
189 $c->authorization_required_response(@args);
a14203f8 190
191 die $Catalyst::DETACH;
a14203f8 192}
193
a14203f8 194sub authorization_required_response {
a14203f8 195 my ( $c, %opts ) = @_;
196
a14203f8 197 $c->res->status(401);
198
ac92fd52 199 # *DONT* short circuit
200 my $ok;
201 $ok++ if $c->_create_digest_auth_response(\%opts);
202 $ok++ if $c->_create_basic_auth_response(\%opts);
a14203f8 203
ac92fd52 204 unless ( $ok ) {
205 die 'Could not build authorization required response. '
206 . 'Did you configure a valid authentication http type: '
207 . 'basic, digest, any';
208 }
a14203f8 209}
210
ac92fd52 211sub _add_authentication_header {
212 my ( $c, $header ) = @_;
213 $c->res->headers->push_header( 'WWW-Authenticate' => $header );
214}
a14203f8 215
ac92fd52 216sub _create_digest_auth_response {
217 my ( $c, $opts ) = @_;
218
219 return unless $c->_is_http_auth_type('digest');
220
221 if ( my $digest = $c->_build_digest_auth_header( $opts ) ) {
222 $c->_add_authentication_header( $digest );
223 return 1;
224 }
a14203f8 225
ac92fd52 226 return;
227}
a14203f8 228
ac92fd52 229sub _create_basic_auth_response {
230 my ( $c, $opts ) = @_;
231
232 return unless $c->_is_http_auth_type('basic');
a14203f8 233
ac92fd52 234 if ( my $basic = $c->_build_basic_auth_header( $opts ) ) {
235 $c->_add_authentication_header( $basic );
236 return 1;
237 }
a14203f8 238
ac92fd52 239 return;
240}
a14203f8 241
ac92fd52 242sub _build_auth_header_realm {
243 my ( $c, $opts ) = @_;
a14203f8 244
245 if ( my $realm = $opts->{realm} ) {
ac92fd52 246 return 'realm=' . String::Escape::qprintable($realm);
247 } else {
248 return;
a14203f8 249 }
ac92fd52 250}
a14203f8 251
ac92fd52 252sub _build_auth_header_domain {
253 my ( $c, $opts ) = @_;
a14203f8 254
255 if ( my $domain = $opts->{domain} ) {
18eacbdd 256 Catalyst::Exception->throw("domain must be an array reference")
a14203f8 257 unless ref($domain) && ref($domain) eq "ARRAY";
258
a14203f8 259 my @uris =
a14203f8 260 $c->config->{authentication}{http}{use_uri_for}
a14203f8 261 ? ( map { $c->uri_for($_) } @$domain )
a14203f8 262 : ( map { URI::Escape::uri_escape($_) } @$domain );
263
ac92fd52 264 return qq{domain="@uris"};
265 } else {
266 return;
a14203f8 267 }
ac92fd52 268}
a14203f8 269
ac92fd52 270sub _build_auth_header_common {
271 my ( $c, $opts ) = @_;
a14203f8 272
ac92fd52 273 return (
274 $c->_build_auth_header_realm($opts),
275 $c->_build_auth_header_domain($opts),
276 );
277}
a14203f8 278
ac92fd52 279sub _build_basic_auth_header {
280 my ( $c, $opts ) = @_;
18eacbdd 281 return $c->_join_auth_header_parts( Basic => $c->_build_auth_header_common( $opts ) );
ac92fd52 282}
a14203f8 283
ac92fd52 284sub _build_digest_auth_header {
285 my ( $c, $opts ) = @_;
a14203f8 286
ac92fd52 287 my $nonce = $c->_digest_auth_nonce($opts);
a14203f8 288
ac92fd52 289 my $key = __PACKAGE__ . '::opaque:' . $nonce->opaque;
290
371a8cc8 291 $c->store_digest_authorization_nonce( $key, $nonce );
a14203f8 292
ac92fd52 293 return $c->_join_auth_header_parts( Digest =>
294 $c->_build_auth_header_common($opts),
295 map { sprintf '%s="%s"', $_, $nonce->$_ } qw(
296 qop
297 nonce
298 opaque
299 algorithm
300 ),
301 );
302}
a14203f8 303
ac92fd52 304sub _digest_auth_nonce {
305 my ( $c, $opts ) = @_;
a14203f8 306
ac92fd52 307 my $package = __PACKAGE__ . '::Nonce';
a14203f8 308
ac92fd52 309 my $nonce = $package->new;
a14203f8 310
371a8cc8 311 if ( my $algorithm = $opts->{algorithm} || $c->config->{authentication}{http}{algorithm}) {
312 $nonce->algorithm( $algorithm );
313 }
a14203f8 314
ac92fd52 315 return $nonce;
316}
a14203f8 317
ac92fd52 318sub _join_auth_header_parts {
319 my ( $c, $type, @parts ) = @_;
320 return "$type " . join(", ", @parts );
321}
a14203f8 322
371a8cc8 323sub get_digest_authorization_nonce {
ac92fd52 324 my ( $c, $key ) = @_;
a14203f8 325
ac92fd52 326 $c->_check_cache;
327 $c->cache->get( $key );
a14203f8 328}
329
371a8cc8 330sub store_digest_authorization_nonce {
ac92fd52 331 my ( $c, $key, $nonce ) = @_;
a14203f8 332
ac92fd52 333 $c->_check_cache;
334 $c->cache->set( $key, $nonce );
335}
a14203f8 336
337package Catalyst::Plugin::Authentication::Credential::HTTP::Nonce;
338
a14203f8 339use strict;
a14203f8 340use base qw[ Class::Accessor::Fast ];
a14203f8 341use Data::UUID ();
342
a14203f8 343our $VERSION = "0.01";
344
a14203f8 345__PACKAGE__->mk_accessors(qw[ nonce nonce_count qop opaque algorithm ]);
346
a14203f8 347sub new {
a14203f8 348 my $class = shift;
a14203f8 349 my $self = $class->SUPER::new(@_);
350
a14203f8 351 $self->nonce( Data::UUID->new->create_b64 );
a14203f8 352 $self->opaque( Data::UUID->new->create_b64 );
a14203f8 353 $self->qop('auth,auth-int');
a14203f8 354 $self->nonce_count('0x0');
a14203f8 355 $self->algorithm('MD5');
356
a14203f8 357 return $self;
a14203f8 358}
359
a14203f8 3601;
361
a14203f8 362__END__
363
a14203f8 364=pod
365
a14203f8 366=head1 NAME
367
a14203f8 368Catalyst::Plugin::Authentication::Credential::HTTP - HTTP Basic and Digest authentication
53306b93 369for Catalyst.
a14203f8 370
a14203f8 371=head1 SYNOPSIS
372
a14203f8 373 use Catalyst qw/
a14203f8 374 Authentication
a14203f8 375 Authentication::Store::Moose
a14203f8 376 Authentication::Credential::HTTP
a14203f8 377 /;
378
3bb378d2 379 __PACKAGE__->config->{authentication}{http}{type} = 'any'; # or 'digest' or 'basic'
380 __PACKAGE__->config->{authentication}{users} = {
381 Mufasa => { password => "Circle Of Life", },
a14203f8 382 };
383
a14203f8 384 sub foo : Local {
a14203f8 385 my ( $self, $c ) = @_;
386
a14203f8 387 $c->authorization_required( realm => "foo" ); # named after the status code ;-)
388
a14203f8 389 # either user gets authenticated or 401 is sent
390
a14203f8 391 do_stuff();
a14203f8 392 }
393
a14203f8 394 # with ACL plugin
a14203f8 395 __PACKAGE__->deny_access_unless("/path", sub { $_[0]->authenticate_http });
396
a14203f8 397 sub end : Private {
a14203f8 398 my ( $self, $c ) = @_;
399
a14203f8 400 $c->authorization_required_response( realm => "foo" );
a14203f8 401 $c->error(0);
a14203f8 402 }
403
a14203f8 404=head1 DESCRIPTION
405
a14203f8 406This moduule lets you use HTTP authentication with
a14203f8 407L<Catalyst::Plugin::Authentication>. Both basic and digest authentication
a14203f8 408are currently supported.
409
a14203f8 410=head1 METHODS
411
a14203f8 412=over 4
413
371a8cc8 414=item authorization_required %opts
a14203f8 415
a14203f8 416Tries to C<authenticate_http>, and if that fails calls
a14203f8 417C<authorization_required_response> and detaches the current action call stack.
418
371a8cc8 419This method just passes the options through untouched.
420
421=item authenticate_http %opts
a14203f8 422
a14203f8 423Looks inside C<< $c->request->headers >> and processes the digest and basic
a14203f8 424(badly named) authorization header.
425
371a8cc8 426This will only try the methods set in the configuration.
427
428See the next two methods for what %opts can contain.
429
430=item authenticate_basic %opts
431
432=item authenticate_digest %opts
433
434Try to authenticate one of the methods without checking if the method is
435allowed in the configuration.
436
437%opts can contain C<store> (either an object or a name), C<user> (to disregard
438%the username from the header altogether, overriding it with a username or user
439%object).
440
441=item authorization_required_response %opts
a14203f8 442
a14203f8 443Sets C<< $c->response >> to the correct status code, and adds the correct
a14203f8 444header to demand authentication data from the user agent.
445
371a8cc8 446Typically used by C<authorization_required>, but may be invoked manually.
447
448%opts can contain C<realm>, C<domain> and C<algorithm>, which are used to build
449%the digest header.
450
451=item store_digest_authorization_nonce $key, $nonce
452
453=item get_digest_authorization_nonce $key
454
455Set or get the C<$nonce> object used by the digest auth mode.
456
457You may override these methods. By default they will call C<get> and C<set> on
458C<< $c->cache >>.
459
460=back
461
462=head1 CONFIGURATION
463
464All configuration is stored in C<< YourApp->config->{authentication}{http} >>.
465
466This should be a hash, and it can contain the following entries:
467
468=over 4
469
470=item store
471
472Either a name or an object -- the default store to use for HTTP authentication.
473
474=item type
475
476Can be either C<any> (the default), C<basic> or C<digest>.
477
478This controls C<authorization_required_response> and C<authenticate_http>, but
479not the "manual" methods.
480
a14203f8 481=back
482
a14203f8 483=head1 AUTHORS
484
a14203f8 485Yuval Kogman, C<nothingmuch@woobling.org>
486
a14203f8 487Jess Robinson
488
a14203f8 489Sascha Kiefer C<esskar@cpan.org>
490
a14203f8 491=head1 COPYRIGHT & LICENSE
492
a14203f8 493 Copyright (c) 2005-2006 the aforementioned authors. All rights
a14203f8 494 reserved. This program is free software; you can redistribute
a14203f8 495 it and/or modify it under the same terms as Perl itself.
496
a14203f8 497=cut