1 package Catalyst::Authentication::Credential::HTTP;
2 use base qw/Catalyst::Authentication::Credential::Password/;
12 __PACKAGE__->mk_accessors(qw/
14 authorization_required_message
23 our $VERSION = '1.009';
26 my ($class, $config, $app, $realm) = @_;
28 $config->{username_field} ||= 'username';
29 # _config is shity back-compat with our base class.
30 my $self = { %$config, _config => $config, _debug => $app->debug };
41 my $type = $self->type || 'any';
43 if (!grep /$type/, ('basic', 'digest', 'any')) {
44 Catalyst::Exception->throw(__PACKAGE__ . " used with unsupported authentication type: " . $type);
50 my ( $self, $c, $realm, $auth_info ) = @_;
53 $auth = $self->authenticate_digest($c, $realm, $auth_info) if $self->_is_http_auth_type('digest');
54 return $auth if $auth;
56 $auth = $self->authenticate_basic($c, $realm, $auth_info) if $self->_is_http_auth_type('basic');
57 return $auth if $auth;
59 $self->authorization_required_response($c, $realm, $auth_info);
60 die $Catalyst::DETACH;
63 sub authenticate_basic {
64 my ( $self, $c, $realm, $auth_info ) = @_;
66 $c->log->debug('Checking http basic authentication.') if $c->debug;
68 my $headers = $c->req->headers;
70 if ( my ( $username, $password ) = $headers->authorization_basic ) {
71 my $user_obj = $realm->find_user( { $self->username_field => $username }, $c);
74 $opts->{$self->password_field} = $password
75 if $self->password_field;
76 if ($self->check_password($user_obj, $opts)) {
80 $c->log->debug("Password mismatch!") if $c->debug;
85 $c->log->debug("Unable to locate user matching user info provided")
94 sub authenticate_digest {
95 my ( $self, $c, $realm, $auth_info ) = @_;
97 $c->log->debug('Checking http digest authentication.') if $c->debug;
99 my $headers = $c->req->headers;
100 my @authorization = $headers->header('Authorization');
101 foreach my $authorization (@authorization) {
102 next unless $authorization =~ m{^Digest};
104 my @key_val = split /=/, $_, 2;
105 $key_val[0] = lc $key_val[0];
106 $key_val[1] =~ s{"}{}g; # remove the quotes
108 } split /,\s?/, substr( $authorization, 7 ); #7 == length "Digest "
110 my $opaque = $res{opaque};
111 my $nonce = $self->get_digest_authorization_nonce( $c, __PACKAGE__ . '::opaque:' . $opaque );
114 $c->log->debug('Checking authentication parameters.')
117 my $uri = $c->request->uri->path_query;
118 my $algorithm = $res{algorithm} || 'MD5';
119 my $nonce_count = '0x' . $res{nc};
121 my $check = $uri eq $res{uri}
122 && ( exists $res{username} )
123 && ( exists $res{qop} )
124 && ( exists $res{cnonce} )
125 && ( exists $res{nc} )
126 && $algorithm eq $nonce->algorithm
127 && hex($nonce_count) > hex( $nonce->nonce_count )
128 && $res{nonce} eq $nonce->nonce; # TODO: set Stale instead
131 $c->log->debug('Digest authentication failed. Bad request.')
133 $c->res->status(400); # bad request
134 Carp::confess $Catalyst::DETACH;
137 $c->log->debug('Checking authentication response.')
140 my $username = $res{username};
144 unless ( $user_obj = $auth_info->{user} ) {
145 $user_obj = $realm->find_user( { $self->username_field => $username }, $c);
147 unless ($user_obj) { # no user, no authentication
148 $c->log->debug("Unable to locate user matching user info provided") if $c->debug;
152 # everything looks good, let's check the response
153 # calculate H(A2) as per spec
154 my $ctx = Digest::MD5->new;
155 $ctx->add( join( ':', $c->request->method, $res{uri} ) );
156 if ( $res{qop} eq 'auth-int' ) {
158 Digest::MD5::md5_hex( $c->request->body ); # not sure here
159 $ctx->add( ':', $digest );
161 my $A2_digest = $ctx->hexdigest;
163 # the idea of the for loop:
164 # if we do not want to store the plain password in our user store,
165 # we can store md5_hex("$username:$realm:$password") instead
166 my $password_field = $self->password_field;
167 for my $r ( 0 .. 1 ) {
168 # calculate H(A1) as per spec
169 my $A1_digest = $r ? $user_obj->$password_field() : do {
170 $ctx = Digest::MD5->new;
171 $ctx->add( join( ':', $username, $realm->name, $user_obj->$password_field() ) );
174 if ( $nonce->algorithm eq 'MD5-sess' ) {
175 $ctx = Digest::MD5->new;
176 $ctx->add( join( ':', $A1_digest, $res{nonce}, $res{cnonce} ) );
177 $A1_digest = $ctx->hexdigest;
180 my $digest_in = join( ':',
181 $A1_digest, $res{nonce},
182 $res{qop} ? ( $res{nc}, $res{cnonce}, $res{qop} ) : (),
184 my $rq_digest = Digest::MD5::md5_hex($digest_in);
185 $nonce->nonce_count($nonce_count);
186 $c->cache->set( __PACKAGE__ . '::opaque:' . $nonce->opaque,
188 if ($rq_digest eq $res{response}) {
199 die "A cache is needed for http digest authentication."
200 unless $c->can('cache');
204 sub _is_http_auth_type {
205 my ( $self, $type ) = @_;
206 my $cfgtype = lc( $self->type );
207 return 1 if $cfgtype eq 'any' || $cfgtype eq lc $type;
211 sub authorization_required_response {
212 my ( $self, $c, $realm, $auth_info ) = @_;
214 $c->res->status(401);
215 $c->res->content_type('text/plain');
216 if (exists $self->{authorization_required_message}) {
217 # If you set the key to undef, don't stamp on the body.
218 $c->res->body($self->authorization_required_message)
219 if defined $self->authorization_required_message;
222 $c->res->body('Authorization required.');
225 # *DONT* short circuit
227 $ok++ if $self->_create_digest_auth_response($c, $auth_info);
228 $ok++ if $self->_create_basic_auth_response($c, $auth_info);
231 die 'Could not build authorization required response. '
232 . 'Did you configure a valid authentication http type: '
233 . 'basic, digest, any';
238 sub _add_authentication_header {
239 my ( $c, $header ) = @_;
240 $c->response->headers->push_header( 'WWW-Authenticate' => $header );
244 sub _create_digest_auth_response {
245 my ( $self, $c, $opts ) = @_;
247 return unless $self->_is_http_auth_type('digest');
249 if ( my $digest = $self->_build_digest_auth_header( $c, $opts ) ) {
250 _add_authentication_header( $c, $digest );
257 sub _create_basic_auth_response {
258 my ( $self, $c, $opts ) = @_;
260 return unless $self->_is_http_auth_type('basic');
262 if ( my $basic = $self->_build_basic_auth_header( $c, $opts ) ) {
263 _add_authentication_header( $c, $basic );
270 sub _build_auth_header_realm {
271 my ( $self, $c, $opts ) = @_;
272 if ( my $realm_name = String::Escape::qprintable($opts->{realm} ? $opts->{realm} : $self->realm->name) ) {
273 $realm_name = qq{"$realm_name"} unless $realm_name =~ /^"/;
274 return 'realm=' . $realm_name;
279 sub _build_auth_header_domain {
280 my ( $self, $c, $opts ) = @_;
281 if ( my $domain = $opts->{domain} ) {
282 Catalyst::Exception->throw("domain must be an array reference")
283 unless ref($domain) && ref($domain) eq "ARRAY";
287 ? ( map { $c->uri_for($_) } @$domain )
288 : ( map { URI::Escape::uri_escape($_) } @$domain );
290 return qq{domain="@uris"};
295 sub _build_auth_header_common {
296 my ( $self, $c, $opts ) = @_;
298 $self->_build_auth_header_realm($c, $opts),
299 $self->_build_auth_header_domain($c, $opts),
303 sub _build_basic_auth_header {
304 my ( $self, $c, $opts ) = @_;
305 return _join_auth_header_parts( Basic => $self->_build_auth_header_common( $c, $opts ) );
308 sub _build_digest_auth_header {
309 my ( $self, $c, $opts ) = @_;
311 my $nonce = $self->_digest_auth_nonce($c, $opts);
313 my $key = __PACKAGE__ . '::opaque:' . $nonce->opaque;
315 $self->store_digest_authorization_nonce( $c, $key, $nonce );
317 return _join_auth_header_parts( Digest =>
318 $self->_build_auth_header_common($c, $opts),
319 map { sprintf '%s="%s"', $_, $nonce->$_ } qw(
328 sub _digest_auth_nonce {
329 my ( $self, $c, $opts ) = @_;
331 my $package = __PACKAGE__ . '::Nonce';
333 my $nonce = $package->new;
335 if ( my $algorithm = $opts->{algorithm} || $self->algorithm) {
336 $nonce->algorithm( $algorithm );
342 sub _join_auth_header_parts {
343 my ( $type, @parts ) = @_;
344 return "$type " . join(", ", @parts );
347 sub get_digest_authorization_nonce {
348 my ( $self, $c, $key ) = @_;
351 return $c->cache->get( $key );
354 sub store_digest_authorization_nonce {
355 my ( $self, $c, $key, $nonce ) = @_;
358 return $c->cache->set( $key, $nonce );
361 package Catalyst::Authentication::Credential::HTTP::Nonce;
364 use base qw[ Class::Accessor::Fast ];
367 our $VERSION = '0.02';
369 __PACKAGE__->mk_accessors(qw[ nonce nonce_count qop opaque algorithm ]);
373 my $self = $class->SUPER::new(@_);
375 $self->nonce( Data::UUID->new->create_b64 );
376 $self->opaque( Data::UUID->new->create_b64 );
377 $self->qop('auth,auth-int');
378 $self->nonce_count('0x0');
379 $self->algorithm('MD5');
392 Catalyst::Authentication::Credential::HTTP - HTTP Basic and Digest authentication
401 __PACKAGE__->config( authentication => {
402 default_realm => 'example',
407 type => 'any', # or 'digest' or 'basic'
408 password_type => 'clear',
409 password_field => 'password'
414 Mufasa => { password => "Circle Of Life", },
422 my ( $self, $c ) = @_;
424 $c->authenticate({ realm => "example" });
425 # either user gets authenticated or 401 is sent
426 # Note that the authentication realm sent to the client (in the
427 # RFC 2617 sense) is overridden here, but this *does not*
428 # effect the Catalyst::Authentication::Realm used for
429 # authentication - to do that, you need
430 # $c->authenticate({}, 'otherrealm')
435 sub always_auth : Local {
436 my ( $self, $c ) = @_;
438 # Force authorization headers onto the response so that the user
439 # is asked again for authentication, even if they successfully
441 my $realm = $c->get_auth_realm('example');
442 $realm->credential->authorization_required_response($c, $realm);
446 __PACKAGE__->deny_access_unless("/path", sub { $_[0]->authenticate });
450 This module lets you use HTTP authentication with
451 L<Catalyst::Plugin::Authentication>. Both basic and digest authentication
452 are currently supported.
454 When authentication is required, this module sets a status of 401, and
455 the body of the response to 'Authorization required.'. To override
456 this and set your own content, check for the C<< $c->res->status ==
457 401 >> in your C<end> action, and change the body accordingly.
465 A nonce is a one-time value sent with each digest authentication
466 request header. The value must always be unique, so per default the
467 last value of the nonce is kept using L<Catalyst::Plugin::Cache>. To
468 change this behaviour, override the
469 C<store_digest_authorization_nonce> and
470 C<get_digest_authorization_nonce> methods as shown below.
478 =item new $config, $c, $realm
484 Validates that $config is ok.
486 =item authenticate $c, $realm, \%auth_info
488 Tries to authenticate the user, and if that fails calls
489 C<authorization_required_response> and detaches the current action call stack.
491 Looks inside C<< $c->request->headers >> and processes the digest and basic
492 (badly named) authorization header.
494 This will only try the methods set in the configuration. First digest, then basic.
496 The %auth_info hash can contain a number of keys which control the authentication behaviour:
502 Sets the HTTP authentication realm presented to the client. Note this does not alter the
503 Catalyst::Authentication::Realm object used for the authentication.
507 Array reference to domains used to build the authorization headers.
509 This list of domains defines the protection space. If a domain URI is an
510 absolute path (starts with /), it is relative to the root URL of the server being accessed.
511 An absolute URI in this list may refer to a different server than the one being accessed.
513 The client will use this list to determine the set of URIs for which the same authentication
514 information may be sent.
516 If this is omitted or its value is empty, the client will assume that the
517 protection space consists of all URIs on the responding server.
519 Therefore, if your application is not hosted at the root of this domain, and you want to
520 prevent the authentication credentials for this application being sent to any other applications.
521 then you should use the I<use_uri_for> configuration option, and pass a domain of I</>.
525 =item authenticate_basic $c, $realm, \%auth_info
527 Performs HTTP basic authentication.
529 =item authenticate_digest $c, $realm, \%auth_info
531 Performs HTTP digest authentication. Note that the password_type B<must> by I<clear> for
532 digest authentication to succeed, and you must have L<Catalyst::Plugin::Session> in
533 your application as digest authentication needs to store persistent data.
535 Note - if you do not want to store your user passwords as clear text, then it is possible
536 to store instead the MD5 digest in hex of the string '$username:$realm:$password'
538 Takes an additional parameter of I<algorithm>, the possible values of which are 'MD5' (the default)
539 and 'MD5-sess'. For more information about 'MD5-sess', see section 3.2.2.2 in RFC 2617.
541 =item authorization_required_response $c, $realm, \%auth_info
543 Sets C<< $c->response >> to the correct status code, and adds the correct
544 header to demand authentication data from the user agent.
546 Typically used by C<authenticate>, but may be invoked manually.
548 %opts can contain C<domain> and C<algorithm>, which are used to build
551 =item store_digest_authorization_nonce $c, $key, $nonce
553 =item get_digest_authorization_nonce $c, $key
555 Set or get the C<$nonce> object used by the digest auth mode.
557 You may override these methods. By default they will call C<get> and C<set> on
564 All configuration is stored in C<< YourApp->config(authentication => { yourrealm => { credential => { class => 'HTTP', %config } } } >>.
566 This should be a hash, and it can contain the following entries:
572 Can be either C<any> (the default), C<basic> or C<digest>.
574 This controls C<authorization_required_response> and C<authenticate>, but
575 not the "manual" methods.
577 =item authorization_required_message
579 Set this to a string to override the default body content "Authorization required.", or set to undef to suppress body content being generated.
583 The type of password returned by the user object. Same usage as in
584 L<Catalyst::Authentication::Credential::Password|Catalyst::Authentication::Credential::Password/password_type>
588 The name of accessor used to retrieve the value of the password field from the user object. Same usage as in
589 L<Catalyst::Authentication::Credential::Password|Catalyst::Authentication::Credential::Password/password_field>
593 The field name that the user's username is mapped into when finding the user from the realm. Defaults to 'username'.
597 If this configuration key has a true value, then the domain(s) for the authorization header will be
598 run through $c->uri_for(). Use this configuration option if your application is not running at the root
599 of your domain, and you want to ensure that authentication credentials from your application are not shared with
600 other applications on the same server.
606 When using digest authentication, this module will only work together
607 with authentication stores whose User objects have a C<password>
608 method that returns the plain-text password. It will not work together
609 with L<Catalyst::Authentication::Store::Htpasswd>, or
610 L<Catalyst::Authentication::Store::DBIC> stores whose
611 C<password> methods return a hashed or salted version of the password.
615 Updated to current name space and currently maintained
616 by: Tomas Doran C<bobtfish@bobtfish.net>.
622 =item Yuval Kogman, C<nothingmuch@woobling.org>
626 =item Sascha Kiefer C<esskar@cpan.org>
632 Patches contributed by:
642 RFC 2617 (or its successors), L<Catalyst::Plugin::Cache>, L<Catalyst::Plugin::Authentication>
644 =head1 COPYRIGHT & LICENSE
646 Copyright (c) 2005-2008 the aforementioned authors. All rights
647 reserved. This program is free software; you can redistribute
648 it and/or modify it under the same terms as Perl itself.