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.017';
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 __PACKAGE__->mk_accessors(qw[ nonce nonce_count qop opaque algorithm ]);
386 my $self = $class->SUPER::new(@_);
388 $self->nonce( Data::UUID->new->create_b64 );
389 $self->opaque( Data::UUID->new->create_b64 );
390 $self->qop('auth,auth-int');
391 $self->nonce_count('0x0');
392 $self->algorithm('MD5');
405 Catalyst::Authentication::Credential::HTTP - HTTP Basic and Digest authentication
414 __PACKAGE__->config( authentication => {
415 default_realm => 'example',
420 type => 'any', # or 'digest' or 'basic'
421 password_type => 'clear',
422 password_field => 'password'
427 Mufasa => { password => "Circle Of Life", },
435 my ( $self, $c ) = @_;
437 $c->authenticate({}, "example");
438 # either user gets authenticated or 401 is sent
439 # Note that the authentication realm sent to the client (in the
440 # RFC 2617 sense) is overridden here, but this *does not*
441 # effect the Catalyst::Authentication::Realm used for
442 # authentication - to do that, you need
443 # $c->authenticate({}, 'otherrealm')
448 sub always_auth : Local {
449 my ( $self, $c ) = @_;
451 # Force authorization headers onto the response so that the user
452 # is asked again for authentication, even if they successfully
454 my $realm = $c->get_auth_realm('example');
455 $realm->credential->authorization_required_response($c, $realm);
459 __PACKAGE__->deny_access_unless("/path", sub { $_[0]->authenticate });
463 This module lets you use HTTP authentication with
464 L<Catalyst::Plugin::Authentication>. Both basic and digest authentication
465 are currently supported.
467 When authentication is required, this module sets a status of 401, and
468 the body of the response to 'Authorization required.'. To override
469 this and set your own content, check for the C<< $c->res->status ==
470 401 >> in your C<end> action, and change the body accordingly.
478 A nonce is a one-time value sent with each digest authentication
479 request header. The value must always be unique, so per default the
480 last value of the nonce is kept using L<Catalyst::Plugin::Cache>. To
481 change this behaviour, override the
482 C<store_digest_authorization_nonce> and
483 C<get_digest_authorization_nonce> methods as shown below.
491 =item new $config, $c, $realm
497 Validates that $config is ok.
499 =item authenticate $c, $realm, \%auth_info
501 Tries to authenticate the user, and if that fails calls
502 C<authorization_required_response> and detaches the current action call stack.
504 Looks inside C<< $c->request->headers >> and processes the digest and basic
505 (badly named) authorization header.
507 This will only try the methods set in the configuration. First digest, then basic.
509 The %auth_info hash can contain a number of keys which control the authentication behaviour:
515 Sets the HTTP authentication realm presented to the client. Note this does not alter the
516 Catalyst::Authentication::Realm object used for the authentication.
520 Array reference to domains used to build the authorization headers.
522 This list of domains defines the protection space. If a domain URI is an
523 absolute path (starts with /), it is relative to the root URL of the server being accessed.
524 An absolute URI in this list may refer to a different server than the one being accessed.
526 The client will use this list to determine the set of URIs for which the same authentication
527 information may be sent.
529 If this is omitted or its value is empty, the client will assume that the
530 protection space consists of all URIs on the responding server.
532 Therefore, if your application is not hosted at the root of this domain, and you want to
533 prevent the authentication credentials for this application being sent to any other applications.
534 then you should use the I<use_uri_for> configuration option, and pass a domain of I</>.
538 =item authenticate_basic $c, $realm, \%auth_info
540 Performs HTTP basic authentication.
542 =item authenticate_digest $c, $realm, \%auth_info
544 Performs HTTP digest authentication.
546 The password_type B<must> be I<clear> for digest authentication to
547 succeed. If you do not want to store your user passwords as clear
548 text, you may instead store the MD5 digest in hex of the string
549 '$username:$realm:$password'.
551 L<Catalyst::Plugin::Cache> is used for persistent storage of the nonce
552 values (see L</Nonce>). It must be loaded in your application, unless
553 you override the C<store_digest_authorization_nonce> and
554 C<get_digest_authorization_nonce> methods as shown below.
556 Takes an additional parameter of I<algorithm>, the possible values of which are 'MD5' (the default)
557 and 'MD5-sess'. For more information about 'MD5-sess', see section 3.2.2.2 in RFC 2617.
559 =item authorization_required_response $c, $realm, \%auth_info
561 Sets C<< $c->response >> to the correct status code, and adds the correct
562 header to demand authentication data from the user agent.
564 Typically used by C<authenticate>, but may be invoked manually.
566 %opts can contain C<domain> and C<algorithm>, which are used to build
569 =item store_digest_authorization_nonce $c, $key, $nonce
571 =item get_digest_authorization_nonce $c, $key
573 Set or get the C<$nonce> object used by the digest auth mode.
575 You may override these methods. By default they will call C<get> and C<set> on
578 =item authentication_failed
580 Sets the 401 response and calls C<< $ctx->detach >>.
586 All configuration is stored in C<< YourApp->config('Plugin::Authentication' => { yourrealm => { credential => { class => 'HTTP', %config } } } >>.
588 This should be a hash, and it can contain the following entries:
594 Can be either C<any> (the default), C<basic> or C<digest>.
596 This controls C<authorization_required_response> and C<authenticate>, but
597 not the "manual" methods.
599 =item authorization_required_message
601 Set this to a string to override the default body content "Authorization required.", or set to undef to suppress body content being generated.
605 The type of password returned by the user object. Same usage as in
606 L<Catalyst::Authentication::Credential::Password|Catalyst::Authentication::Credential::Password/password_type>
610 The name of accessor used to retrieve the value of the password field from the user object. Same usage as in
611 L<Catalyst::Authentication::Credential::Password|Catalyst::Authentication::Credential::Password/password_field>
615 The field name that the user's username is mapped into when finding the user from the realm. Defaults to 'username'.
619 If this configuration key has a true value, then the domain(s) for the authorization header will be
620 run through $c->uri_for(). Use this configuration option if your application is not running at the root
621 of your domain, and you want to ensure that authentication credentials from your application are not shared with
622 other applications on the same server.
626 If this configuration key has a true value then authentication will be denied
627 (and a 401 issued in normal circumstances) unless the request is via https.
629 =item no_unprompted_authorization_required
631 Causes authentication to fail as normal modules do, without calling
632 C<< $c->detach >>. This means that the basic auth credential can be used as
633 part of the progressive realm.
635 However use like this is probably not optimum it also means that users in
636 browsers ill never get a HTTP authenticate dialogue box (unless you manually
637 return a 401 response in your application), and even some automated
638 user agents (for APIs) will not send the Authorization header without
639 specific manipulation of the request headers.
641 =item broken_dotnet_digest_without_query_string
643 Enables support for .NET (or other similarly broken clients), which
644 fails to include the query string in the uri in the digest
645 Authorization header, contrary to rfc2617.
647 This option has no effect on clients that include the query string;
648 they will continue to work as normal.
654 When using digest authentication, this module will only work together
655 with authentication stores whose User objects have a C<password>
656 method that returns the plain-text password. It will not work together
657 with L<Catalyst::Authentication::Store::Htpasswd>, or
658 L<Catalyst::Authentication::Store::DBIC> stores whose
659 C<password> methods return a hashed or salted version of the password.
663 Updated to current name space and currently maintained
664 by: Tomas Doran C<bobtfish@bobtfish.net>.
670 =item Yuval Kogman, C<nothingmuch@woobling.org>
674 =item Sascha Kiefer C<esskar@cpan.org>
680 Patches contributed by:
686 =item Devin Austin (dhoss) C<dhoss@cpan.org>
688 =item Ronald J Kimball
694 RFC 2617 (or its successors), L<Catalyst::Plugin::Cache>, L<Catalyst::Plugin::Authentication>
696 =head1 COPYRIGHT & LICENSE
698 Copyright (c) 2005-2008 the aforementioned authors. All rights
699 reserved. This program is free software; you can redistribute
700 it and/or modify it under the same terms as Perl itself.