1 package Catalyst::Authentication::Credential::HTTP;
2 use base qw/Catalyst::Authentication::Credential::Password/;
12 __PACKAGE__->mk_accessors(qw/
14 authorization_required_message
21 no_unprompted_authorization_required
23 broken_dotnet_digest_without_query_string
26 our $VERSION = '1.015';
29 my ($class, $config, $app, $realm) = @_;
31 $config->{username_field} ||= 'username';
32 # _config is shity back-compat with our base class.
33 my $self = { %$config, _config => $config, _debug => $app->debug ? 1 : 0 };
44 my $type = $self->type || 'any';
46 if (!grep /$type/, ('basic', 'digest', 'any')) {
47 Catalyst::Exception->throw(__PACKAGE__ . " used with unsupported authentication type: " . $type);
53 my ( $self, $c, $realm, $auth_info ) = @_;
56 $self->authentication_failed( $c, $realm, $auth_info )
57 if $self->require_ssl ? $c->req->base->scheme ne 'https' : 0;
59 $auth = $self->authenticate_digest($c, $realm, $auth_info) if $self->_is_http_auth_type('digest');
60 return $auth if $auth;
62 $auth = $self->authenticate_basic($c, $realm, $auth_info) if $self->_is_http_auth_type('basic');
63 return $auth if $auth;
65 $self->authentication_failed( $c, $realm, $auth_info );
68 sub authentication_failed {
69 my ( $self, $c, $realm, $auth_info ) = @_;
70 unless ($self->no_unprompted_authorization_required) {
71 $self->authorization_required_response($c, $realm, $auth_info);
72 die $Catalyst::DETACH;
76 sub authenticate_basic {
77 my ( $self, $c, $realm, $auth_info ) = @_;
79 $c->log->debug('Checking http basic authentication.') if $c->debug;
81 my $headers = $c->req->headers;
83 if ( my ( $username, $password ) = $headers->authorization_basic ) {
84 my $user_obj = $realm->find_user( { $self->username_field => $username }, $c);
87 $opts->{$self->password_field} = $password
88 if $self->password_field;
89 if ($self->check_password($user_obj, $opts)) {
93 $c->log->debug("Password mismatch!") if $c->debug;
98 $c->log->debug("Unable to locate user matching user info provided")
107 sub authenticate_digest {
108 my ( $self, $c, $realm, $auth_info ) = @_;
110 $c->log->debug('Checking http digest authentication.') if $c->debug;
112 my $headers = $c->req->headers;
113 my @authorization = $headers->header('Authorization');
114 foreach my $authorization (@authorization) {
115 next unless $authorization =~ m{^Digest};
117 my @key_val = split /=/, $_, 2;
118 $key_val[0] = lc $key_val[0];
119 $key_val[1] =~ s{"}{}g; # remove the quotes
121 } split /,\s?/, substr( $authorization, 7 ); #7 == length "Digest "
123 my $opaque = $res{opaque};
124 my $nonce = $self->get_digest_authorization_nonce( $c, __PACKAGE__ . '::opaque:' . $opaque );
127 $c->log->debug('Checking authentication parameters.')
130 my $uri = $c->request->uri->path_query;
131 my $algorithm = $res{algorithm} || 'MD5';
132 my $nonce_count = '0x' . $res{nc};
134 my $check = ($uri eq $res{uri} ||
135 ($self->broken_dotnet_digest_without_query_string &&
136 $c->request->uri->path eq $res{uri}))
137 && ( exists $res{username} )
138 && ( exists $res{qop} )
139 && ( exists $res{cnonce} )
140 && ( exists $res{nc} )
141 && $algorithm eq $nonce->algorithm
142 && hex($nonce_count) > hex( $nonce->nonce_count )
143 && $res{nonce} eq $nonce->nonce; # TODO: set Stale instead
146 $c->log->debug('Digest authentication failed. Bad request.')
148 $c->res->status(400); # bad request
149 Carp::confess $Catalyst::DETACH;
152 $c->log->debug('Checking authentication response.')
155 my $username = $res{username};
159 unless ( $user_obj = $auth_info->{user} ) {
160 $user_obj = $realm->find_user( { $self->username_field => $username }, $c);
162 unless ($user_obj) { # no user, no authentication
163 $c->log->debug("Unable to locate user matching user info provided") if $c->debug;
167 # everything looks good, let's check the response
168 # calculate H(A2) as per spec
169 my $ctx = Digest::MD5->new;
170 $ctx->add( join( ':', $c->request->method, $res{uri} ) );
171 if ( $res{qop} eq 'auth-int' ) {
173 Digest::MD5::md5_hex( $c->request->body ); # not sure here
174 $ctx->add( ':', $digest );
176 my $A2_digest = $ctx->hexdigest;
178 # the idea of the for loop:
179 # if we do not want to store the plain password in our user store,
180 # we can store md5_hex("$username:$realm:$password") instead
181 my $password_field = $self->password_field;
182 for my $r ( 0 .. 1 ) {
183 # calculate H(A1) as per spec
184 my $A1_digest = $r ? $user_obj->$password_field() : do {
185 $ctx = Digest::MD5->new;
186 $ctx->add( join( ':', $username, $realm->name, $user_obj->$password_field() ) );
189 if ( $nonce->algorithm eq 'MD5-sess' ) {
190 $ctx = Digest::MD5->new;
191 $ctx->add( join( ':', $A1_digest, $res{nonce}, $res{cnonce} ) );
192 $A1_digest = $ctx->hexdigest;
195 my $digest_in = join( ':',
196 $A1_digest, $res{nonce},
197 $res{qop} ? ( $res{nc}, $res{cnonce}, $res{qop} ) : (),
199 my $rq_digest = Digest::MD5::md5_hex($digest_in);
200 $nonce->nonce_count($nonce_count);
201 my $key = __PACKAGE__ . '::opaque:' . $nonce->opaque;
202 $self->store_digest_authorization_nonce( $c, $key, $nonce );
203 if ($rq_digest eq $res{response}) {
214 die "A cache is needed for http digest authentication."
215 unless $c->can('cache');
219 sub _is_http_auth_type {
220 my ( $self, $type ) = @_;
221 my $cfgtype = lc( $self->type );
222 return 1 if $cfgtype eq 'any' || $cfgtype eq lc $type;
226 sub authorization_required_response {
227 my ( $self, $c, $realm, $auth_info ) = @_;
229 $c->res->status(401);
230 $c->res->content_type('text/plain');
231 if (exists $self->{authorization_required_message}) {
232 # If you set the key to undef, don't stamp on the body.
233 $c->res->body($self->authorization_required_message)
234 if defined $self->authorization_required_message;
237 $c->res->body('Authorization required.');
240 # *DONT* short circuit
242 $ok++ if $self->_create_digest_auth_response($c, $auth_info);
243 $ok++ if $self->_create_basic_auth_response($c, $auth_info);
246 die 'Could not build authorization required response. '
247 . 'Did you configure a valid authentication http type: '
248 . 'basic, digest, any';
253 sub _add_authentication_header {
254 my ( $c, $header ) = @_;
255 $c->response->headers->push_header( 'WWW-Authenticate' => $header );
259 sub _create_digest_auth_response {
260 my ( $self, $c, $opts ) = @_;
262 return unless $self->_is_http_auth_type('digest');
264 if ( my $digest = $self->_build_digest_auth_header( $c, $opts ) ) {
265 _add_authentication_header( $c, $digest );
272 sub _create_basic_auth_response {
273 my ( $self, $c, $opts ) = @_;
275 return unless $self->_is_http_auth_type('basic');
277 if ( my $basic = $self->_build_basic_auth_header( $c, $opts ) ) {
278 _add_authentication_header( $c, $basic );
285 sub _build_auth_header_realm {
286 my ( $self, $c, $opts ) = @_;
287 if ( my $realm_name = String::Escape::qprintable($opts->{realm} ? $opts->{realm} : $self->realm->name) ) {
288 $realm_name = qq{"$realm_name"} unless $realm_name =~ /^"/;
289 return 'realm=' . $realm_name;
294 sub _build_auth_header_domain {
295 my ( $self, $c, $opts ) = @_;
296 if ( my $domain = $opts->{domain} ) {
297 Catalyst::Exception->throw("domain must be an array reference")
298 unless ref($domain) && ref($domain) eq "ARRAY";
302 ? ( map { $c->uri_for($_) } @$domain )
303 : ( map { URI::Escape::uri_escape($_) } @$domain );
305 return qq{domain="@uris"};
310 sub _build_auth_header_common {
311 my ( $self, $c, $opts ) = @_;
313 $self->_build_auth_header_realm($c, $opts),
314 $self->_build_auth_header_domain($c, $opts),
318 sub _build_basic_auth_header {
319 my ( $self, $c, $opts ) = @_;
320 return _join_auth_header_parts( Basic => $self->_build_auth_header_common( $c, $opts ) );
323 sub _build_digest_auth_header {
324 my ( $self, $c, $opts ) = @_;
326 my $nonce = $self->_digest_auth_nonce($c, $opts);
328 my $key = __PACKAGE__ . '::opaque:' . $nonce->opaque;
330 $self->store_digest_authorization_nonce( $c, $key, $nonce );
332 return _join_auth_header_parts( Digest =>
333 $self->_build_auth_header_common($c, $opts),
334 map { sprintf '%s="%s"', $_, $nonce->$_ } qw(
343 sub _digest_auth_nonce {
344 my ( $self, $c, $opts ) = @_;
346 my $package = __PACKAGE__ . '::Nonce';
348 my $nonce = $package->new;
350 if ( my $algorithm = $opts->{algorithm} || $self->algorithm) {
351 $nonce->algorithm( $algorithm );
357 sub _join_auth_header_parts {
358 my ( $type, @parts ) = @_;
359 return "$type " . join(", ", @parts );
362 sub get_digest_authorization_nonce {
363 my ( $self, $c, $key ) = @_;
366 return $c->cache->get( $key );
369 sub store_digest_authorization_nonce {
370 my ( $self, $c, $key, $nonce ) = @_;
373 return $c->cache->set( $key, $nonce );
376 package Catalyst::Authentication::Credential::HTTP::Nonce;
379 use base qw[ Class::Accessor::Fast ];
382 our $VERSION = '0.02';
384 __PACKAGE__->mk_accessors(qw[ nonce nonce_count qop opaque algorithm ]);
388 my $self = $class->SUPER::new(@_);
390 $self->nonce( Data::UUID->new->create_b64 );
391 $self->opaque( Data::UUID->new->create_b64 );
392 $self->qop('auth,auth-int');
393 $self->nonce_count('0x0');
394 $self->algorithm('MD5');
407 Catalyst::Authentication::Credential::HTTP - HTTP Basic and Digest authentication
416 __PACKAGE__->config( authentication => {
417 default_realm => 'example',
422 type => 'any', # or 'digest' or 'basic'
423 password_type => 'clear',
424 password_field => 'password'
429 Mufasa => { password => "Circle Of Life", },
437 my ( $self, $c ) = @_;
439 $c->authenticate({}, "example");
440 # either user gets authenticated or 401 is sent
441 # Note that the authentication realm sent to the client (in the
442 # RFC 2617 sense) is overridden here, but this *does not*
443 # effect the Catalyst::Authentication::Realm used for
444 # authentication - to do that, you need
445 # $c->authenticate({}, 'otherrealm')
450 sub always_auth : Local {
451 my ( $self, $c ) = @_;
453 # Force authorization headers onto the response so that the user
454 # is asked again for authentication, even if they successfully
456 my $realm = $c->get_auth_realm('example');
457 $realm->credential->authorization_required_response($c, $realm);
461 __PACKAGE__->deny_access_unless("/path", sub { $_[0]->authenticate });
465 This module lets you use HTTP authentication with
466 L<Catalyst::Plugin::Authentication>. Both basic and digest authentication
467 are currently supported.
469 When authentication is required, this module sets a status of 401, and
470 the body of the response to 'Authorization required.'. To override
471 this and set your own content, check for the C<< $c->res->status ==
472 401 >> in your C<end> action, and change the body accordingly.
480 A nonce is a one-time value sent with each digest authentication
481 request header. The value must always be unique, so per default the
482 last value of the nonce is kept using L<Catalyst::Plugin::Cache>. To
483 change this behaviour, override the
484 C<store_digest_authorization_nonce> and
485 C<get_digest_authorization_nonce> methods as shown below.
493 =item new $config, $c, $realm
499 Validates that $config is ok.
501 =item authenticate $c, $realm, \%auth_info
503 Tries to authenticate the user, and if that fails calls
504 C<authorization_required_response> and detaches the current action call stack.
506 Looks inside C<< $c->request->headers >> and processes the digest and basic
507 (badly named) authorization header.
509 This will only try the methods set in the configuration. First digest, then basic.
511 The %auth_info hash can contain a number of keys which control the authentication behaviour:
517 Sets the HTTP authentication realm presented to the client. Note this does not alter the
518 Catalyst::Authentication::Realm object used for the authentication.
522 Array reference to domains used to build the authorization headers.
524 This list of domains defines the protection space. If a domain URI is an
525 absolute path (starts with /), it is relative to the root URL of the server being accessed.
526 An absolute URI in this list may refer to a different server than the one being accessed.
528 The client will use this list to determine the set of URIs for which the same authentication
529 information may be sent.
531 If this is omitted or its value is empty, the client will assume that the
532 protection space consists of all URIs on the responding server.
534 Therefore, if your application is not hosted at the root of this domain, and you want to
535 prevent the authentication credentials for this application being sent to any other applications.
536 then you should use the I<use_uri_for> configuration option, and pass a domain of I</>.
540 =item authenticate_basic $c, $realm, \%auth_info
542 Performs HTTP basic authentication.
544 =item authenticate_digest $c, $realm, \%auth_info
546 Performs HTTP digest authentication.
548 The password_type B<must> be I<clear> for digest authentication to
549 succeed. If you do not want to store your user passwords as clear
550 text, you may instead store the MD5 digest in hex of the string
551 '$username:$realm:$password'.
553 L<Catalyst::Plugin::Cache> is used for persistent storage of the nonce
554 values (see L</Nonce>). It must be loaded in your application, unless
555 you override the C<store_digest_authorization_nonce> and
556 C<get_digest_authorization_nonce> methods as shown below.
558 Takes an additional parameter of I<algorithm>, the possible values of which are 'MD5' (the default)
559 and 'MD5-sess'. For more information about 'MD5-sess', see section 3.2.2.2 in RFC 2617.
561 =item authorization_required_response $c, $realm, \%auth_info
563 Sets C<< $c->response >> to the correct status code, and adds the correct
564 header to demand authentication data from the user agent.
566 Typically used by C<authenticate>, but may be invoked manually.
568 %opts can contain C<domain> and C<algorithm>, which are used to build
571 =item store_digest_authorization_nonce $c, $key, $nonce
573 =item get_digest_authorization_nonce $c, $key
575 Set or get the C<$nonce> object used by the digest auth mode.
577 You may override these methods. By default they will call C<get> and C<set> on
580 =item authentication_failed
582 Sets the 401 response and calls C<< $ctx->detach >>.
588 All configuration is stored in C<< YourApp->config('Plugin::Authentication' => { yourrealm => { credential => { class => 'HTTP', %config } } } >>.
590 This should be a hash, and it can contain the following entries:
596 Can be either C<any> (the default), C<basic> or C<digest>.
598 This controls C<authorization_required_response> and C<authenticate>, but
599 not the "manual" methods.
601 =item authorization_required_message
603 Set this to a string to override the default body content "Authorization required.", or set to undef to suppress body content being generated.
607 The type of password returned by the user object. Same usage as in
608 L<Catalyst::Authentication::Credential::Password|Catalyst::Authentication::Credential::Password/password_type>
612 The name of accessor used to retrieve the value of the password field from the user object. Same usage as in
613 L<Catalyst::Authentication::Credential::Password|Catalyst::Authentication::Credential::Password/password_field>
617 The field name that the user's username is mapped into when finding the user from the realm. Defaults to 'username'.
621 If this configuration key has a true value, then the domain(s) for the authorization header will be
622 run through $c->uri_for(). Use this configuration option if your application is not running at the root
623 of your domain, and you want to ensure that authentication credentials from your application are not shared with
624 other applications on the same server.
628 If this configuration key has a true value then authentication will be denied
629 (and a 401 issued in normal circumstances) unless the request is via https.
631 =item no_unprompted_authorization_required
633 Causes authentication to fail as normal modules do, without calling
634 C<< $c->detach >>. This means that the basic auth credential can be used as
635 part of the progressive realm.
637 However use like this is probably not optimum it also means that users in
638 browsers ill never get a HTTP authenticate dialogue box (unless you manually
639 return a 401 response in your application), and even some automated
640 user agents (for APIs) will not send the Authorization header without
641 specific manipulation of the request headers.
643 =item broken_dotnet_digest_without_query_string
645 Enables support for .NET (or other similarly broken clients), which
646 fails to include the query string in the uri in the digest
647 Authorization header, contrary to rfc2617.
649 This option has no effect on clients that include the query string;
650 they will continue to work as normal.
656 When using digest authentication, this module will only work together
657 with authentication stores whose User objects have a C<password>
658 method that returns the plain-text password. It will not work together
659 with L<Catalyst::Authentication::Store::Htpasswd>, or
660 L<Catalyst::Authentication::Store::DBIC> stores whose
661 C<password> methods return a hashed or salted version of the password.
665 Updated to current name space and currently maintained
666 by: Tomas Doran C<bobtfish@bobtfish.net>.
672 =item Yuval Kogman, C<nothingmuch@woobling.org>
676 =item Sascha Kiefer C<esskar@cpan.org>
682 Patches contributed by:
688 =item Devin Austin (dhoss) C<dhoss@cpan.org>
690 =item Ronald J Kimball
696 RFC 2617 (or its successors), L<Catalyst::Plugin::Cache>, L<Catalyst::Plugin::Authentication>
698 =head1 COPYRIGHT & LICENSE
700 Copyright (c) 2005-2008 the aforementioned authors. All rights
701 reserved. This program is free software; you can redistribute
702 it and/or modify it under the same terms as Perl itself.