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
25 our $VERSION = '1.014';
28 my ($class, $config, $app, $realm) = @_;
30 $config->{username_field} ||= 'username';
31 # _config is shity back-compat with our base class.
32 my $self = { %$config, _config => $config, _debug => $app->debug ? 1 : 0 };
43 my $type = $self->type || 'any';
45 if (!grep /$type/, ('basic', 'digest', 'any')) {
46 Catalyst::Exception->throw(__PACKAGE__ . " used with unsupported authentication type: " . $type);
52 my ( $self, $c, $realm, $auth_info ) = @_;
55 $self->authentication_failed( $c, $realm, $auth_info )
56 if $self->require_ssl ? $c->req->base->scheme ne 'https' : 0;
58 $auth = $self->authenticate_digest($c, $realm, $auth_info) if $self->_is_http_auth_type('digest');
59 return $auth if $auth;
61 $auth = $self->authenticate_basic($c, $realm, $auth_info) if $self->_is_http_auth_type('basic');
62 return $auth if $auth;
64 $self->authentication_failed( $c, $realm, $auth_info );
67 sub authentication_failed {
68 my ( $self, $c, $realm, $auth_info ) = @_;
69 unless ($self->no_unprompted_authorization_required) {
70 $self->authorization_required_response($c, $realm, $auth_info);
71 die $Catalyst::DETACH;
75 sub authenticate_basic {
76 my ( $self, $c, $realm, $auth_info ) = @_;
78 $c->log->debug('Checking http basic authentication.') if $c->debug;
80 my $headers = $c->req->headers;
82 if ( my ( $username, $password ) = $headers->authorization_basic ) {
83 my $user_obj = $realm->find_user( { $self->username_field => $username }, $c);
86 $opts->{$self->password_field} = $password
87 if $self->password_field;
88 if ($self->check_password($user_obj, $opts)) {
92 $c->log->debug("Password mismatch!") if $c->debug;
97 $c->log->debug("Unable to locate user matching user info provided")
106 sub authenticate_digest {
107 my ( $self, $c, $realm, $auth_info ) = @_;
109 $c->log->debug('Checking http digest authentication.') if $c->debug;
111 my $headers = $c->req->headers;
112 my @authorization = $headers->header('Authorization');
113 foreach my $authorization (@authorization) {
114 next unless $authorization =~ m{^Digest};
116 my @key_val = split /=/, $_, 2;
117 $key_val[0] = lc $key_val[0];
118 $key_val[1] =~ s{"}{}g; # remove the quotes
120 } split /,\s?/, substr( $authorization, 7 ); #7 == length "Digest "
122 my $opaque = $res{opaque};
123 my $nonce = $self->get_digest_authorization_nonce( $c, __PACKAGE__ . '::opaque:' . $opaque );
126 $c->log->debug('Checking authentication parameters.')
129 my $uri = $c->request->uri->path_query;
130 my $algorithm = $res{algorithm} || 'MD5';
131 my $nonce_count = '0x' . $res{nc};
133 my $check = $uri eq $res{uri}
134 && ( exists $res{username} )
135 && ( exists $res{qop} )
136 && ( exists $res{cnonce} )
137 && ( exists $res{nc} )
138 && $algorithm eq $nonce->algorithm
139 && hex($nonce_count) > hex( $nonce->nonce_count )
140 && $res{nonce} eq $nonce->nonce; # TODO: set Stale instead
143 $c->log->debug('Digest authentication failed. Bad request.')
145 $c->res->status(400); # bad request
146 Carp::confess $Catalyst::DETACH;
149 $c->log->debug('Checking authentication response.')
152 my $username = $res{username};
156 unless ( $user_obj = $auth_info->{user} ) {
157 $user_obj = $realm->find_user( { $self->username_field => $username }, $c);
159 unless ($user_obj) { # no user, no authentication
160 $c->log->debug("Unable to locate user matching user info provided") if $c->debug;
164 # everything looks good, let's check the response
165 # calculate H(A2) as per spec
166 my $ctx = Digest::MD5->new;
167 $ctx->add( join( ':', $c->request->method, $res{uri} ) );
168 if ( $res{qop} eq 'auth-int' ) {
170 Digest::MD5::md5_hex( $c->request->body ); # not sure here
171 $ctx->add( ':', $digest );
173 my $A2_digest = $ctx->hexdigest;
175 # the idea of the for loop:
176 # if we do not want to store the plain password in our user store,
177 # we can store md5_hex("$username:$realm:$password") instead
178 my $password_field = $self->password_field;
179 for my $r ( 0 .. 1 ) {
180 # calculate H(A1) as per spec
181 my $A1_digest = $r ? $user_obj->$password_field() : do {
182 $ctx = Digest::MD5->new;
183 $ctx->add( join( ':', $username, $realm->name, $user_obj->$password_field() ) );
186 if ( $nonce->algorithm eq 'MD5-sess' ) {
187 $ctx = Digest::MD5->new;
188 $ctx->add( join( ':', $A1_digest, $res{nonce}, $res{cnonce} ) );
189 $A1_digest = $ctx->hexdigest;
192 my $digest_in = join( ':',
193 $A1_digest, $res{nonce},
194 $res{qop} ? ( $res{nc}, $res{cnonce}, $res{qop} ) : (),
196 my $rq_digest = Digest::MD5::md5_hex($digest_in);
197 $nonce->nonce_count($nonce_count);
198 my $key = __PACKAGE__ . '::opaque:' . $nonce->opaque;
199 $self->store_digest_authorization_nonce( $c, $key, $nonce );
200 if ($rq_digest eq $res{response}) {
211 die "A cache is needed for http digest authentication."
212 unless $c->can('cache');
216 sub _is_http_auth_type {
217 my ( $self, $type ) = @_;
218 my $cfgtype = lc( $self->type );
219 return 1 if $cfgtype eq 'any' || $cfgtype eq lc $type;
223 sub authorization_required_response {
224 my ( $self, $c, $realm, $auth_info ) = @_;
226 $c->res->status(401);
227 $c->res->content_type('text/plain');
228 if (exists $self->{authorization_required_message}) {
229 # If you set the key to undef, don't stamp on the body.
230 $c->res->body($self->authorization_required_message)
231 if defined $self->authorization_required_message;
234 $c->res->body('Authorization required.');
237 # *DONT* short circuit
239 $ok++ if $self->_create_digest_auth_response($c, $auth_info);
240 $ok++ if $self->_create_basic_auth_response($c, $auth_info);
243 die 'Could not build authorization required response. '
244 . 'Did you configure a valid authentication http type: '
245 . 'basic, digest, any';
250 sub _add_authentication_header {
251 my ( $c, $header ) = @_;
252 $c->response->headers->push_header( 'WWW-Authenticate' => $header );
256 sub _create_digest_auth_response {
257 my ( $self, $c, $opts ) = @_;
259 return unless $self->_is_http_auth_type('digest');
261 if ( my $digest = $self->_build_digest_auth_header( $c, $opts ) ) {
262 _add_authentication_header( $c, $digest );
269 sub _create_basic_auth_response {
270 my ( $self, $c, $opts ) = @_;
272 return unless $self->_is_http_auth_type('basic');
274 if ( my $basic = $self->_build_basic_auth_header( $c, $opts ) ) {
275 _add_authentication_header( $c, $basic );
282 sub _build_auth_header_realm {
283 my ( $self, $c, $opts ) = @_;
284 if ( my $realm_name = String::Escape::qprintable($opts->{realm} ? $opts->{realm} : $self->realm->name) ) {
285 $realm_name = qq{"$realm_name"} unless $realm_name =~ /^"/;
286 return 'realm=' . $realm_name;
291 sub _build_auth_header_domain {
292 my ( $self, $c, $opts ) = @_;
293 if ( my $domain = $opts->{domain} ) {
294 Catalyst::Exception->throw("domain must be an array reference")
295 unless ref($domain) && ref($domain) eq "ARRAY";
299 ? ( map { $c->uri_for($_) } @$domain )
300 : ( map { URI::Escape::uri_escape($_) } @$domain );
302 return qq{domain="@uris"};
307 sub _build_auth_header_common {
308 my ( $self, $c, $opts ) = @_;
310 $self->_build_auth_header_realm($c, $opts),
311 $self->_build_auth_header_domain($c, $opts),
315 sub _build_basic_auth_header {
316 my ( $self, $c, $opts ) = @_;
317 return _join_auth_header_parts( Basic => $self->_build_auth_header_common( $c, $opts ) );
320 sub _build_digest_auth_header {
321 my ( $self, $c, $opts ) = @_;
323 my $nonce = $self->_digest_auth_nonce($c, $opts);
325 my $key = __PACKAGE__ . '::opaque:' . $nonce->opaque;
327 $self->store_digest_authorization_nonce( $c, $key, $nonce );
329 return _join_auth_header_parts( Digest =>
330 $self->_build_auth_header_common($c, $opts),
331 map { sprintf '%s="%s"', $_, $nonce->$_ } qw(
340 sub _digest_auth_nonce {
341 my ( $self, $c, $opts ) = @_;
343 my $package = __PACKAGE__ . '::Nonce';
345 my $nonce = $package->new;
347 if ( my $algorithm = $opts->{algorithm} || $self->algorithm) {
348 $nonce->algorithm( $algorithm );
354 sub _join_auth_header_parts {
355 my ( $type, @parts ) = @_;
356 return "$type " . join(", ", @parts );
359 sub get_digest_authorization_nonce {
360 my ( $self, $c, $key ) = @_;
363 return $c->cache->get( $key );
366 sub store_digest_authorization_nonce {
367 my ( $self, $c, $key, $nonce ) = @_;
370 return $c->cache->set( $key, $nonce );
373 package Catalyst::Authentication::Credential::HTTP::Nonce;
376 use base qw[ Class::Accessor::Fast ];
379 our $VERSION = '0.02';
381 __PACKAGE__->mk_accessors(qw[ nonce nonce_count qop opaque algorithm ]);
385 my $self = $class->SUPER::new(@_);
387 $self->nonce( Data::UUID->new->create_b64 );
388 $self->opaque( Data::UUID->new->create_b64 );
389 $self->qop('auth,auth-int');
390 $self->nonce_count('0x0');
391 $self->algorithm('MD5');
404 Catalyst::Authentication::Credential::HTTP - HTTP Basic and Digest authentication
413 __PACKAGE__->config( authentication => {
414 default_realm => 'example',
419 type => 'any', # or 'digest' or 'basic'
420 password_type => 'clear',
421 password_field => 'password'
426 Mufasa => { password => "Circle Of Life", },
434 my ( $self, $c ) = @_;
436 $c->authenticate({ realm => "example" });
437 # either user gets authenticated or 401 is sent
438 # Note that the authentication realm sent to the client (in the
439 # RFC 2617 sense) is overridden here, but this *does not*
440 # effect the Catalyst::Authentication::Realm used for
441 # authentication - to do that, you need
442 # $c->authenticate({}, 'otherrealm')
447 sub always_auth : Local {
448 my ( $self, $c ) = @_;
450 # Force authorization headers onto the response so that the user
451 # is asked again for authentication, even if they successfully
453 my $realm = $c->get_auth_realm('example');
454 $realm->credential->authorization_required_response($c, $realm);
458 __PACKAGE__->deny_access_unless("/path", sub { $_[0]->authenticate });
462 This module lets you use HTTP authentication with
463 L<Catalyst::Plugin::Authentication>. Both basic and digest authentication
464 are currently supported.
466 When authentication is required, this module sets a status of 401, and
467 the body of the response to 'Authorization required.'. To override
468 this and set your own content, check for the C<< $c->res->status ==
469 401 >> in your C<end> action, and change the body accordingly.
477 A nonce is a one-time value sent with each digest authentication
478 request header. The value must always be unique, so per default the
479 last value of the nonce is kept using L<Catalyst::Plugin::Cache>. To
480 change this behaviour, override the
481 C<store_digest_authorization_nonce> and
482 C<get_digest_authorization_nonce> methods as shown below.
490 =item new $config, $c, $realm
496 Validates that $config is ok.
498 =item authenticate $c, $realm, \%auth_info
500 Tries to authenticate the user, and if that fails calls
501 C<authorization_required_response> and detaches the current action call stack.
503 Looks inside C<< $c->request->headers >> and processes the digest and basic
504 (badly named) authorization header.
506 This will only try the methods set in the configuration. First digest, then basic.
508 The %auth_info hash can contain a number of keys which control the authentication behaviour:
514 Sets the HTTP authentication realm presented to the client. Note this does not alter the
515 Catalyst::Authentication::Realm object used for the authentication.
519 Array reference to domains used to build the authorization headers.
521 This list of domains defines the protection space. If a domain URI is an
522 absolute path (starts with /), it is relative to the root URL of the server being accessed.
523 An absolute URI in this list may refer to a different server than the one being accessed.
525 The client will use this list to determine the set of URIs for which the same authentication
526 information may be sent.
528 If this is omitted or its value is empty, the client will assume that the
529 protection space consists of all URIs on the responding server.
531 Therefore, if your application is not hosted at the root of this domain, and you want to
532 prevent the authentication credentials for this application being sent to any other applications.
533 then you should use the I<use_uri_for> configuration option, and pass a domain of I</>.
537 =item authenticate_basic $c, $realm, \%auth_info
539 Performs HTTP basic authentication.
541 =item authenticate_digest $c, $realm, \%auth_info
543 Performs HTTP digest authentication.
545 The password_type B<must> be I<clear> for digest authentication to
546 succeed. If you do not want to store your user passwords as clear
547 text, you may instead store the MD5 digest in hex of the string
548 '$username:$realm:$password'.
550 L<Catalyst::Plugin::Cache> is used for persistent storage of the nonce
551 values (see L</Nonce>). It must be loaded in your application, unless
552 you override the C<store_digest_authorization_nonce> and
553 C<get_digest_authorization_nonce> methods as shown below.
555 Takes an additional parameter of I<algorithm>, the possible values of which are 'MD5' (the default)
556 and 'MD5-sess'. For more information about 'MD5-sess', see section 3.2.2.2 in RFC 2617.
558 =item authorization_required_response $c, $realm, \%auth_info
560 Sets C<< $c->response >> to the correct status code, and adds the correct
561 header to demand authentication data from the user agent.
563 Typically used by C<authenticate>, but may be invoked manually.
565 %opts can contain C<domain> and C<algorithm>, which are used to build
568 =item store_digest_authorization_nonce $c, $key, $nonce
570 =item get_digest_authorization_nonce $c, $key
572 Set or get the C<$nonce> object used by the digest auth mode.
574 You may override these methods. By default they will call C<get> and C<set> on
577 =item authentication_failed
579 Sets the 401 response and calls C<< $ctx->detach >>.
585 All configuration is stored in C<< YourApp->config('Plugin::Authentication' => { yourrealm => { credential => { class => 'HTTP', %config } } } >>.
587 This should be a hash, and it can contain the following entries:
593 Can be either C<any> (the default), C<basic> or C<digest>.
595 This controls C<authorization_required_response> and C<authenticate>, but
596 not the "manual" methods.
598 =item authorization_required_message
600 Set this to a string to override the default body content "Authorization required.", or set to undef to suppress body content being generated.
604 The type of password returned by the user object. Same usage as in
605 L<Catalyst::Authentication::Credential::Password|Catalyst::Authentication::Credential::Password/password_type>
609 The name of accessor used to retrieve the value of the password field from the user object. Same usage as in
610 L<Catalyst::Authentication::Credential::Password|Catalyst::Authentication::Credential::Password/password_field>
614 The field name that the user's username is mapped into when finding the user from the realm. Defaults to 'username'.
618 If this configuration key has a true value, then the domain(s) for the authorization header will be
619 run through $c->uri_for(). Use this configuration option if your application is not running at the root
620 of your domain, and you want to ensure that authentication credentials from your application are not shared with
621 other applications on the same server.
625 If this configuration key has a true value then authentication will be denied
626 (and a 401 issued in normal circumstances) unless the request is via https.
628 =item no_unprompted_authorization_required
630 Causes authentication to fail as normal modules do, without calling
631 C<< $c->detach >>. This means that the basic auth credential can be used as
632 part of the progressive realm.
634 However use like this is probably not optimum it also means that users in
635 browsers ill never get a HTTP authenticate dialogue box (unless you manually
636 return a 401 response in your application), and even some automated
637 user agents (for APIs) will not send the Authorization header without
638 specific manipulation of the request headers.
644 When using digest authentication, this module will only work together
645 with authentication stores whose User objects have a C<password>
646 method that returns the plain-text password. It will not work together
647 with L<Catalyst::Authentication::Store::Htpasswd>, or
648 L<Catalyst::Authentication::Store::DBIC> stores whose
649 C<password> methods return a hashed or salted version of the password.
653 Updated to current name space and currently maintained
654 by: Tomas Doran C<bobtfish@bobtfish.net>.
660 =item Yuval Kogman, C<nothingmuch@woobling.org>
664 =item Sascha Kiefer C<esskar@cpan.org>
670 Patches contributed by:
676 =item Devin Austin (dhoss) C<dhoss@cpan.org>
682 RFC 2617 (or its successors), L<Catalyst::Plugin::Cache>, L<Catalyst::Plugin::Authentication>
684 =head1 COPYRIGHT & LICENSE
686 Copyright (c) 2005-2008 the aforementioned authors. All rights
687 reserved. This program is free software; you can redistribute
688 it and/or modify it under the same terms as Perl itself.