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.019';
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 # hide from PAUSE
379 Catalyst::Authentication::Credential::HTTP::Nonce;
382 use base qw[ Class::Accessor::Fast ];
383 use Data::UUID 0.11 ();
385 __PACKAGE__->mk_accessors(qw[ nonce nonce_count qop opaque algorithm ]);
389 my $self = $class->SUPER::new(@_);
391 $self->nonce( Data::UUID->new->create_b64 );
392 $self->opaque( Data::UUID->new->create_b64 );
393 $self->qop('auth,auth-int');
394 $self->nonce_count('0x0');
395 $self->algorithm('MD5');
418 __PACKAGE__->config( authentication => {
419 default_realm => 'example',
424 type => 'any', # or 'digest' or 'basic'
425 password_type => 'clear',
426 password_field => 'password'
431 Mufasa => { password => "Circle Of Life", },
439 my ( $self, $c ) = @_;
441 $c->authenticate({}, "example");
442 # either user gets authenticated or 401 is sent
443 # Note that the authentication realm sent to the client (in the
444 # RFC 2617 sense) is overridden here, but this *does not*
445 # effect the Catalyst::Authentication::Realm used for
446 # authentication - to do that, you need
447 # $c->authenticate({}, 'otherrealm')
452 sub always_auth : Local {
453 my ( $self, $c ) = @_;
455 # Force authorization headers onto the response so that the user
456 # is asked again for authentication, even if they successfully
458 my $realm = $c->get_auth_realm('example');
459 $realm->credential->authorization_required_response($c, $realm);
463 __PACKAGE__->deny_access_unless("/path", sub { $_[0]->authenticate });
467 This module lets you use HTTP authentication with
468 L<Catalyst::Plugin::Authentication>. Both basic and digest authentication
469 are currently supported.
471 When authentication is required, this module sets a status of 401, and
472 the body of the response to 'Authorization required.'. To override
473 this and set your own content, check for the C<< $c->res->status ==
474 401 >> in your C<end> action, and change the body accordingly.
482 A nonce is a one-time value sent with each digest authentication
483 request header. The value must always be unique, so per default the
484 last value of the nonce is kept using L<Catalyst::Plugin::Cache>. To
485 change this behaviour, override the
486 C<store_digest_authorization_nonce> and
487 C<get_digest_authorization_nonce> methods as shown below.
495 =item new $config, $c, $realm
501 Validates that $config is ok.
503 =item authenticate $c, $realm, \%auth_info
505 Tries to authenticate the user, and if that fails calls
506 C<authorization_required_response> and detaches the current action call stack.
508 Looks inside C<< $c->request->headers >> and processes the digest and basic
509 (badly named) authorization header.
511 This will only try the methods set in the configuration. First digest, then basic.
513 The %auth_info hash can contain a number of keys which control the authentication behaviour:
519 Sets the HTTP authentication realm presented to the client. Note this does not alter the
520 Catalyst::Authentication::Realm object used for the authentication.
524 Array reference to domains used to build the authorization headers.
526 This list of domains defines the protection space. If a domain URI is an
527 absolute path (starts with /), it is relative to the root URL of the server being accessed.
528 An absolute URI in this list may refer to a different server than the one being accessed.
530 The client will use this list to determine the set of URIs for which the same authentication
531 information may be sent.
533 If this is omitted or its value is empty, the client will assume that the
534 protection space consists of all URIs on the responding server.
536 Therefore, if your application is not hosted at the root of this domain, and you want to
537 prevent the authentication credentials for this application being sent to any other applications.
538 then you should use the I<use_uri_for> configuration option, and pass a domain of I</>.
542 =item authenticate_basic $c, $realm, \%auth_info
544 Performs HTTP basic authentication.
546 =item authenticate_digest $c, $realm, \%auth_info
548 Performs HTTP digest authentication.
550 The password_type B<must> be I<clear> for digest authentication to
551 succeed. If you do not want to store your user passwords as clear
552 text, you may instead store the MD5 digest in hex of the string
553 '$username:$realm:$password'.
555 L<Catalyst::Plugin::Cache> is used for persistent storage of the nonce
556 values (see L</Nonce>). It must be loaded in your application, unless
557 you override the C<store_digest_authorization_nonce> and
558 C<get_digest_authorization_nonce> methods as shown below.
560 Takes an additional parameter of I<algorithm>, the possible values of which are 'MD5' (the default)
561 and 'MD5-sess'. For more information about 'MD5-sess', see section 3.2.2.2 in RFC 2617.
563 =item authorization_required_response $c, $realm, \%auth_info
565 Sets C<< $c->response >> to the correct status code, and adds the correct
566 header to demand authentication data from the user agent.
568 Typically used by C<authenticate>, but may be invoked manually.
570 %opts can contain C<domain> and C<algorithm>, which are used to build
573 =item store_digest_authorization_nonce $c, $key, $nonce
575 =item get_digest_authorization_nonce $c, $key
577 Set or get the C<$nonce> object used by the digest auth mode.
579 You may override these methods. By default they will call C<get> and C<set> on
582 =item authentication_failed
584 Sets the 401 response and calls C<< $ctx->detach >>.
590 All configuration is stored in C<< YourApp->config('Plugin::Authentication' => { yourrealm => { credential => { class => 'HTTP', %config } } } >>.
592 This should be a hash, and it can contain the following entries:
598 Can be either C<any> (the default), C<basic> or C<digest>.
600 This controls C<authorization_required_response> and C<authenticate>, but
601 not the "manual" methods.
603 =item authorization_required_message
605 Set this to a string to override the default body content "Authorization required.", or set to undef to suppress body content being generated.
609 The type of password returned by the user object. Same usage as in
610 L<Catalyst::Authentication::Credential::Password|Catalyst::Authentication::Credential::Password/password_type>
614 The name of accessor used to retrieve the value of the password field from the user object. Same usage as in
615 L<Catalyst::Authentication::Credential::Password|Catalyst::Authentication::Credential::Password/password_field>
619 The field name that the user's username is mapped into when finding the user from the realm. Defaults to 'username'.
623 If this configuration key has a true value, then the domain(s) for the authorization header will be
624 run through $c->uri_for(). Use this configuration option if your application is not running at the root
625 of your domain, and you want to ensure that authentication credentials from your application are not shared with
626 other applications on the same server.
630 If this configuration key has a true value then authentication will be denied
631 (and a 401 issued in normal circumstances) unless the request is via https.
633 =item no_unprompted_authorization_required
635 Causes authentication to fail as normal modules do, without calling
636 C<< $c->detach >>. This means that the basic auth credential can be used as
637 part of the progressive realm.
639 However use like this is probably not optimum it also means that users in
640 browsers ill never get a HTTP authenticate dialogue box (unless you manually
641 return a 401 response in your application), and even some automated
642 user agents (for APIs) will not send the Authorization header without
643 specific manipulation of the request headers.
645 =item broken_dotnet_digest_without_query_string
647 Enables support for .NET (or other similarly broken clients), which
648 fails to include the query string in the uri in the digest
649 Authorization header, contrary to rfc2617.
651 This option has no effect on clients that include the query string;
652 they will continue to work as normal.
658 When using digest authentication, this module will only work together
659 with authentication stores whose User objects have a C<password>
660 method that returns the plain-text password. It will not work together
661 with L<Catalyst::Authentication::Store::Htpasswd>, or
662 L<Catalyst::Authentication::Store::DBIC> stores whose
663 C<password> methods return a hashed or salted version of the password.
667 RFC 2617 (or its successors), L<Catalyst::Plugin::Cache>, L<Catalyst::Plugin::Authentication>