1 package Catalyst::Authentication::Credential::HTTP;
2 # ABSTRACT: HTTP Basic and Digest authentication for Catalyst
4 use base qw/Catalyst::Authentication::Credential::Password/;
14 __PACKAGE__->mk_accessors(qw/
16 authorization_required_message
23 no_unprompted_authorization_required
25 broken_dotnet_digest_without_query_string
28 our $VERSION = '1.017';
31 my ($class, $config, $app, $realm) = @_;
33 $config->{username_field} ||= 'username';
34 # _config is shity back-compat with our base class.
35 my $self = { %$config, _config => $config, _debug => $app->debug ? 1 : 0 };
46 my $type = $self->type || 'any';
48 if (!grep /$type/, ('basic', 'digest', 'any')) {
49 Catalyst::Exception->throw(__PACKAGE__ . " used with unsupported authentication type: " . $type);
55 my ( $self, $c, $realm, $auth_info ) = @_;
58 $self->authentication_failed( $c, $realm, $auth_info )
59 if $self->require_ssl ? $c->req->base->scheme ne 'https' : 0;
61 $auth = $self->authenticate_digest($c, $realm, $auth_info) if $self->_is_http_auth_type('digest');
62 return $auth if $auth;
64 $auth = $self->authenticate_basic($c, $realm, $auth_info) if $self->_is_http_auth_type('basic');
65 return $auth if $auth;
67 $self->authentication_failed( $c, $realm, $auth_info );
70 sub authentication_failed {
71 my ( $self, $c, $realm, $auth_info ) = @_;
72 unless ($self->no_unprompted_authorization_required) {
73 $self->authorization_required_response($c, $realm, $auth_info);
74 die $Catalyst::DETACH;
78 sub authenticate_basic {
79 my ( $self, $c, $realm, $auth_info ) = @_;
81 $c->log->debug('Checking http basic authentication.') if $c->debug;
83 my $headers = $c->req->headers;
85 if ( my ( $username, $password ) = $headers->authorization_basic ) {
86 my $user_obj = $realm->find_user( { $self->username_field => $username }, $c);
89 $opts->{$self->password_field} = $password
90 if $self->password_field;
91 if ($self->check_password($user_obj, $opts)) {
95 $c->log->debug("Password mismatch!") if $c->debug;
100 $c->log->debug("Unable to locate user matching user info provided")
109 sub authenticate_digest {
110 my ( $self, $c, $realm, $auth_info ) = @_;
112 $c->log->debug('Checking http digest authentication.') if $c->debug;
114 my $headers = $c->req->headers;
115 my @authorization = $headers->header('Authorization');
116 foreach my $authorization (@authorization) {
117 next unless $authorization =~ m{^Digest};
119 my @key_val = split /=/, $_, 2;
120 $key_val[0] = lc $key_val[0];
121 $key_val[1] =~ s{"}{}g; # remove the quotes
123 } split /,\s?/, substr( $authorization, 7 ); #7 == length "Digest "
125 my $opaque = $res{opaque};
126 my $nonce = $self->get_digest_authorization_nonce( $c, __PACKAGE__ . '::opaque:' . $opaque );
129 $c->log->debug('Checking authentication parameters.')
132 my $uri = $c->request->uri->path_query;
133 my $algorithm = $res{algorithm} || 'MD5';
134 my $nonce_count = '0x' . $res{nc};
136 my $check = ($uri eq $res{uri} ||
137 ($self->broken_dotnet_digest_without_query_string &&
138 $c->request->uri->path eq $res{uri}))
139 && ( exists $res{username} )
140 && ( exists $res{qop} )
141 && ( exists $res{cnonce} )
142 && ( exists $res{nc} )
143 && $algorithm eq $nonce->algorithm
144 && hex($nonce_count) > hex( $nonce->nonce_count )
145 && $res{nonce} eq $nonce->nonce; # TODO: set Stale instead
148 $c->log->debug('Digest authentication failed. Bad request.')
150 $c->res->status(400); # bad request
151 Carp::confess $Catalyst::DETACH;
154 $c->log->debug('Checking authentication response.')
157 my $username = $res{username};
161 unless ( $user_obj = $auth_info->{user} ) {
162 $user_obj = $realm->find_user( { $self->username_field => $username }, $c);
164 unless ($user_obj) { # no user, no authentication
165 $c->log->debug("Unable to locate user matching user info provided") if $c->debug;
169 # everything looks good, let's check the response
170 # calculate H(A2) as per spec
171 my $ctx = Digest::MD5->new;
172 $ctx->add( join( ':', $c->request->method, $res{uri} ) );
173 if ( $res{qop} eq 'auth-int' ) {
175 Digest::MD5::md5_hex( $c->request->body ); # not sure here
176 $ctx->add( ':', $digest );
178 my $A2_digest = $ctx->hexdigest;
180 # the idea of the for loop:
181 # if we do not want to store the plain password in our user store,
182 # we can store md5_hex("$username:$realm:$password") instead
183 my $password_field = $self->password_field;
184 for my $r ( 0 .. 1 ) {
185 # calculate H(A1) as per spec
186 my $A1_digest = $r ? $user_obj->$password_field() : do {
187 $ctx = Digest::MD5->new;
188 $ctx->add( join( ':', $username, $realm->name, $user_obj->$password_field() ) );
191 if ( $nonce->algorithm eq 'MD5-sess' ) {
192 $ctx = Digest::MD5->new;
193 $ctx->add( join( ':', $A1_digest, $res{nonce}, $res{cnonce} ) );
194 $A1_digest = $ctx->hexdigest;
197 my $digest_in = join( ':',
198 $A1_digest, $res{nonce},
199 $res{qop} ? ( $res{nc}, $res{cnonce}, $res{qop} ) : (),
201 my $rq_digest = Digest::MD5::md5_hex($digest_in);
202 $nonce->nonce_count($nonce_count);
203 my $key = __PACKAGE__ . '::opaque:' . $nonce->opaque;
204 $self->store_digest_authorization_nonce( $c, $key, $nonce );
205 if ($rq_digest eq $res{response}) {
216 die "A cache is needed for http digest authentication."
217 unless $c->can('cache');
221 sub _is_http_auth_type {
222 my ( $self, $type ) = @_;
223 my $cfgtype = lc( $self->type );
224 return 1 if $cfgtype eq 'any' || $cfgtype eq lc $type;
228 sub authorization_required_response {
229 my ( $self, $c, $realm, $auth_info ) = @_;
231 $c->res->status(401);
232 $c->res->content_type('text/plain');
233 if (exists $self->{authorization_required_message}) {
234 # If you set the key to undef, don't stamp on the body.
235 $c->res->body($self->authorization_required_message)
236 if defined $self->authorization_required_message;
239 $c->res->body('Authorization required.');
242 # *DONT* short circuit
244 $ok++ if $self->_create_digest_auth_response($c, $auth_info);
245 $ok++ if $self->_create_basic_auth_response($c, $auth_info);
248 die 'Could not build authorization required response. '
249 . 'Did you configure a valid authentication http type: '
250 . 'basic, digest, any';
255 sub _add_authentication_header {
256 my ( $c, $header ) = @_;
257 $c->response->headers->push_header( 'WWW-Authenticate' => $header );
261 sub _create_digest_auth_response {
262 my ( $self, $c, $opts ) = @_;
264 return unless $self->_is_http_auth_type('digest');
266 if ( my $digest = $self->_build_digest_auth_header( $c, $opts ) ) {
267 _add_authentication_header( $c, $digest );
274 sub _create_basic_auth_response {
275 my ( $self, $c, $opts ) = @_;
277 return unless $self->_is_http_auth_type('basic');
279 if ( my $basic = $self->_build_basic_auth_header( $c, $opts ) ) {
280 _add_authentication_header( $c, $basic );
287 sub _build_auth_header_realm {
288 my ( $self, $c, $opts ) = @_;
289 if ( my $realm_name = String::Escape::qprintable($opts->{realm} ? $opts->{realm} : $self->realm->name) ) {
290 $realm_name = qq{"$realm_name"} unless $realm_name =~ /^"/;
291 return 'realm=' . $realm_name;
296 sub _build_auth_header_domain {
297 my ( $self, $c, $opts ) = @_;
298 if ( my $domain = $opts->{domain} ) {
299 Catalyst::Exception->throw("domain must be an array reference")
300 unless ref($domain) && ref($domain) eq "ARRAY";
304 ? ( map { $c->uri_for($_) } @$domain )
305 : ( map { URI::Escape::uri_escape($_) } @$domain );
307 return qq{domain="@uris"};
312 sub _build_auth_header_common {
313 my ( $self, $c, $opts ) = @_;
315 $self->_build_auth_header_realm($c, $opts),
316 $self->_build_auth_header_domain($c, $opts),
320 sub _build_basic_auth_header {
321 my ( $self, $c, $opts ) = @_;
322 return _join_auth_header_parts( Basic => $self->_build_auth_header_common( $c, $opts ) );
325 sub _build_digest_auth_header {
326 my ( $self, $c, $opts ) = @_;
328 my $nonce = $self->_digest_auth_nonce($c, $opts);
330 my $key = __PACKAGE__ . '::opaque:' . $nonce->opaque;
332 $self->store_digest_authorization_nonce( $c, $key, $nonce );
334 return _join_auth_header_parts( Digest =>
335 $self->_build_auth_header_common($c, $opts),
336 map { sprintf '%s="%s"', $_, $nonce->$_ } qw(
345 sub _digest_auth_nonce {
346 my ( $self, $c, $opts ) = @_;
348 my $package = __PACKAGE__ . '::Nonce';
350 my $nonce = $package->new;
352 if ( my $algorithm = $opts->{algorithm} || $self->algorithm) {
353 $nonce->algorithm( $algorithm );
359 sub _join_auth_header_parts {
360 my ( $type, @parts ) = @_;
361 return "$type " . join(", ", @parts );
364 sub get_digest_authorization_nonce {
365 my ( $self, $c, $key ) = @_;
368 return $c->cache->get( $key );
371 sub store_digest_authorization_nonce {
372 my ( $self, $c, $key, $nonce ) = @_;
375 return $c->cache->set( $key, $nonce );
378 package Catalyst::Authentication::Credential::HTTP::Nonce;
381 use base qw[ Class::Accessor::Fast ];
382 use Data::UUID 0.11 ();
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');
417 __PACKAGE__->config( authentication => {
418 default_realm => 'example',
423 type => 'any', # or 'digest' or 'basic'
424 password_type => 'clear',
425 password_field => 'password'
430 Mufasa => { password => "Circle Of Life", },
438 my ( $self, $c ) = @_;
440 $c->authenticate({}, "example");
441 # either user gets authenticated or 401 is sent
442 # Note that the authentication realm sent to the client (in the
443 # RFC 2617 sense) is overridden here, but this *does not*
444 # effect the Catalyst::Authentication::Realm used for
445 # authentication - to do that, you need
446 # $c->authenticate({}, 'otherrealm')
451 sub always_auth : Local {
452 my ( $self, $c ) = @_;
454 # Force authorization headers onto the response so that the user
455 # is asked again for authentication, even if they successfully
457 my $realm = $c->get_auth_realm('example');
458 $realm->credential->authorization_required_response($c, $realm);
462 __PACKAGE__->deny_access_unless("/path", sub { $_[0]->authenticate });
466 This module lets you use HTTP authentication with
467 L<Catalyst::Plugin::Authentication>. Both basic and digest authentication
468 are currently supported.
470 When authentication is required, this module sets a status of 401, and
471 the body of the response to 'Authorization required.'. To override
472 this and set your own content, check for the C<< $c->res->status ==
473 401 >> in your C<end> action, and change the body accordingly.
481 A nonce is a one-time value sent with each digest authentication
482 request header. The value must always be unique, so per default the
483 last value of the nonce is kept using L<Catalyst::Plugin::Cache>. To
484 change this behaviour, override the
485 C<store_digest_authorization_nonce> and
486 C<get_digest_authorization_nonce> methods as shown below.
494 =item new $config, $c, $realm
500 Validates that $config is ok.
502 =item authenticate $c, $realm, \%auth_info
504 Tries to authenticate the user, and if that fails calls
505 C<authorization_required_response> and detaches the current action call stack.
507 Looks inside C<< $c->request->headers >> and processes the digest and basic
508 (badly named) authorization header.
510 This will only try the methods set in the configuration. First digest, then basic.
512 The %auth_info hash can contain a number of keys which control the authentication behaviour:
518 Sets the HTTP authentication realm presented to the client. Note this does not alter the
519 Catalyst::Authentication::Realm object used for the authentication.
523 Array reference to domains used to build the authorization headers.
525 This list of domains defines the protection space. If a domain URI is an
526 absolute path (starts with /), it is relative to the root URL of the server being accessed.
527 An absolute URI in this list may refer to a different server than the one being accessed.
529 The client will use this list to determine the set of URIs for which the same authentication
530 information may be sent.
532 If this is omitted or its value is empty, the client will assume that the
533 protection space consists of all URIs on the responding server.
535 Therefore, if your application is not hosted at the root of this domain, and you want to
536 prevent the authentication credentials for this application being sent to any other applications.
537 then you should use the I<use_uri_for> configuration option, and pass a domain of I</>.
541 =item authenticate_basic $c, $realm, \%auth_info
543 Performs HTTP basic authentication.
545 =item authenticate_digest $c, $realm, \%auth_info
547 Performs HTTP digest authentication.
549 The password_type B<must> be I<clear> for digest authentication to
550 succeed. If you do not want to store your user passwords as clear
551 text, you may instead store the MD5 digest in hex of the string
552 '$username:$realm:$password'.
554 L<Catalyst::Plugin::Cache> is used for persistent storage of the nonce
555 values (see L</Nonce>). It must be loaded in your application, unless
556 you override the C<store_digest_authorization_nonce> and
557 C<get_digest_authorization_nonce> methods as shown below.
559 Takes an additional parameter of I<algorithm>, the possible values of which are 'MD5' (the default)
560 and 'MD5-sess'. For more information about 'MD5-sess', see section 3.2.2.2 in RFC 2617.
562 =item authorization_required_response $c, $realm, \%auth_info
564 Sets C<< $c->response >> to the correct status code, and adds the correct
565 header to demand authentication data from the user agent.
567 Typically used by C<authenticate>, but may be invoked manually.
569 %opts can contain C<domain> and C<algorithm>, which are used to build
572 =item store_digest_authorization_nonce $c, $key, $nonce
574 =item get_digest_authorization_nonce $c, $key
576 Set or get the C<$nonce> object used by the digest auth mode.
578 You may override these methods. By default they will call C<get> and C<set> on
581 =item authentication_failed
583 Sets the 401 response and calls C<< $ctx->detach >>.
589 All configuration is stored in C<< YourApp->config('Plugin::Authentication' => { yourrealm => { credential => { class => 'HTTP', %config } } } >>.
591 This should be a hash, and it can contain the following entries:
597 Can be either C<any> (the default), C<basic> or C<digest>.
599 This controls C<authorization_required_response> and C<authenticate>, but
600 not the "manual" methods.
602 =item authorization_required_message
604 Set this to a string to override the default body content "Authorization required.", or set to undef to suppress body content being generated.
608 The type of password returned by the user object. Same usage as in
609 L<Catalyst::Authentication::Credential::Password|Catalyst::Authentication::Credential::Password/password_type>
613 The name of accessor used to retrieve the value of the password field from the user object. Same usage as in
614 L<Catalyst::Authentication::Credential::Password|Catalyst::Authentication::Credential::Password/password_field>
618 The field name that the user's username is mapped into when finding the user from the realm. Defaults to 'username'.
622 If this configuration key has a true value, then the domain(s) for the authorization header will be
623 run through $c->uri_for(). Use this configuration option if your application is not running at the root
624 of your domain, and you want to ensure that authentication credentials from your application are not shared with
625 other applications on the same server.
629 If this configuration key has a true value then authentication will be denied
630 (and a 401 issued in normal circumstances) unless the request is via https.
632 =item no_unprompted_authorization_required
634 Causes authentication to fail as normal modules do, without calling
635 C<< $c->detach >>. This means that the basic auth credential can be used as
636 part of the progressive realm.
638 However use like this is probably not optimum it also means that users in
639 browsers ill never get a HTTP authenticate dialogue box (unless you manually
640 return a 401 response in your application), and even some automated
641 user agents (for APIs) will not send the Authorization header without
642 specific manipulation of the request headers.
644 =item broken_dotnet_digest_without_query_string
646 Enables support for .NET (or other similarly broken clients), which
647 fails to include the query string in the uri in the digest
648 Authorization header, contrary to rfc2617.
650 This option has no effect on clients that include the query string;
651 they will continue to work as normal.
657 When using digest authentication, this module will only work together
658 with authentication stores whose User objects have a C<password>
659 method that returns the plain-text password. It will not work together
660 with L<Catalyst::Authentication::Store::Htpasswd>, or
661 L<Catalyst::Authentication::Store::DBIC> stores whose
662 C<password> methods return a hashed or salted version of the password.
666 RFC 2617 (or its successors), L<Catalyst::Plugin::Cache>, L<Catalyst::Plugin::Authentication>