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;
84 $c->log->debug("Unable to locate user matching user info provided")
88 $c->log->debug("Unable to locate user matching user info provided") if $c->debug;
96 sub authenticate_digest {
97 my ( $self, $c, $realm, $auth_info ) = @_;
99 $c->log->debug('Checking http digest authentication.') if $c->debug;
101 my $headers = $c->req->headers;
102 my @authorization = $headers->header('Authorization');
103 foreach my $authorization (@authorization) {
104 next unless $authorization =~ m{^Digest};
106 my @key_val = split /=/, $_, 2;
107 $key_val[0] = lc $key_val[0];
108 $key_val[1] =~ s{"}{}g; # remove the quotes
110 } split /,\s?/, substr( $authorization, 7 ); #7 == length "Digest "
112 my $opaque = $res{opaque};
113 my $nonce = $self->get_digest_authorization_nonce( $c, __PACKAGE__ . '::opaque:' . $opaque );
116 $c->log->debug('Checking authentication parameters.')
119 my $uri = $c->request->uri->path_query;
120 my $algorithm = $res{algorithm} || 'MD5';
121 my $nonce_count = '0x' . $res{nc};
123 my $check = $uri eq $res{uri}
124 && ( exists $res{username} )
125 && ( exists $res{qop} )
126 && ( exists $res{cnonce} )
127 && ( exists $res{nc} )
128 && $algorithm eq $nonce->algorithm
129 && hex($nonce_count) > hex( $nonce->nonce_count )
130 && $res{nonce} eq $nonce->nonce; # TODO: set Stale instead
133 $c->log->debug('Digest authentication failed. Bad request.')
135 $c->res->status(400); # bad request
136 Carp::confess $Catalyst::DETACH;
139 $c->log->debug('Checking authentication response.')
142 my $username = $res{username};
146 unless ( $user_obj = $auth_info->{user} ) {
147 $user_obj = $realm->find_user( { $self->username_field => $username }, $c);
149 unless ($user_obj) { # no user, no authentication
150 $c->log->debug("Unable to locate user matching user info provided") if $c->debug;
154 # everything looks good, let's check the response
155 # calculate H(A2) as per spec
156 my $ctx = Digest::MD5->new;
157 $ctx->add( join( ':', $c->request->method, $res{uri} ) );
158 if ( $res{qop} eq 'auth-int' ) {
160 Digest::MD5::md5_hex( $c->request->body ); # not sure here
161 $ctx->add( ':', $digest );
163 my $A2_digest = $ctx->hexdigest;
165 # the idea of the for loop:
166 # if we do not want to store the plain password in our user store,
167 # we can store md5_hex("$username:$realm:$password") instead
168 my $password_field = $self->password_field;
169 for my $r ( 0 .. 1 ) {
170 # calculate H(A1) as per spec
171 my $A1_digest = $r ? $user_obj->$password_field() : do {
172 $ctx = Digest::MD5->new;
173 $ctx->add( join( ':', $username, $realm->name, $user_obj->$password_field() ) );
176 if ( $nonce->algorithm eq 'MD5-sess' ) {
177 $ctx = Digest::MD5->new;
178 $ctx->add( join( ':', $A1_digest, $res{nonce}, $res{cnonce} ) );
179 $A1_digest = $ctx->hexdigest;
182 my $digest_in = join( ':',
183 $A1_digest, $res{nonce},
184 $res{qop} ? ( $res{nc}, $res{cnonce}, $res{qop} ) : (),
186 my $rq_digest = Digest::MD5::md5_hex($digest_in);
187 $nonce->nonce_count($nonce_count);
188 $c->cache->set( __PACKAGE__ . '::opaque:' . $nonce->opaque,
190 if ($rq_digest eq $res{response}) {
201 die "A cache is needed for http digest authentication."
202 unless $c->can('cache');
206 sub _is_http_auth_type {
207 my ( $self, $type ) = @_;
208 my $cfgtype = lc( $self->type );
209 return 1 if $cfgtype eq 'any' || $cfgtype eq lc $type;
213 sub authorization_required_response {
214 my ( $self, $c, $realm, $auth_info ) = @_;
216 $c->res->status(401);
217 $c->res->content_type('text/plain');
218 if (exists $self->{authorization_required_message}) {
219 # If you set the key to undef, don't stamp on the body.
220 $c->res->body($self->authorization_required_message)
221 if defined $self->authorization_required_message;
224 $c->res->body('Authorization required.');
227 # *DONT* short circuit
229 $ok++ if $self->_create_digest_auth_response($c, $auth_info);
230 $ok++ if $self->_create_basic_auth_response($c, $auth_info);
233 die 'Could not build authorization required response. '
234 . 'Did you configure a valid authentication http type: '
235 . 'basic, digest, any';
240 sub _add_authentication_header {
241 my ( $c, $header ) = @_;
242 $c->response->headers->push_header( 'WWW-Authenticate' => $header );
246 sub _create_digest_auth_response {
247 my ( $self, $c, $opts ) = @_;
249 return unless $self->_is_http_auth_type('digest');
251 if ( my $digest = $self->_build_digest_auth_header( $c, $opts ) ) {
252 _add_authentication_header( $c, $digest );
259 sub _create_basic_auth_response {
260 my ( $self, $c, $opts ) = @_;
262 return unless $self->_is_http_auth_type('basic');
264 if ( my $basic = $self->_build_basic_auth_header( $c, $opts ) ) {
265 _add_authentication_header( $c, $basic );
272 sub _build_auth_header_realm {
273 my ( $self, $c, $opts ) = @_;
274 if ( my $realm_name = String::Escape::qprintable($opts->{realm} ? $opts->{realm} : $self->realm->name) ) {
275 $realm_name = qq{"$realm_name"} unless $realm_name =~ /^"/;
276 return 'realm=' . $realm_name;
281 sub _build_auth_header_domain {
282 my ( $self, $c, $opts ) = @_;
283 if ( my $domain = $opts->{domain} ) {
284 Catalyst::Exception->throw("domain must be an array reference")
285 unless ref($domain) && ref($domain) eq "ARRAY";
289 ? ( map { $c->uri_for($_) } @$domain )
290 : ( map { URI::Escape::uri_escape($_) } @$domain );
292 return qq{domain="@uris"};
297 sub _build_auth_header_common {
298 my ( $self, $c, $opts ) = @_;
300 $self->_build_auth_header_realm($c, $opts),
301 $self->_build_auth_header_domain($c, $opts),
305 sub _build_basic_auth_header {
306 my ( $self, $c, $opts ) = @_;
307 return _join_auth_header_parts( Basic => $self->_build_auth_header_common( $c, $opts ) );
310 sub _build_digest_auth_header {
311 my ( $self, $c, $opts ) = @_;
313 my $nonce = $self->_digest_auth_nonce($c, $opts);
315 my $key = __PACKAGE__ . '::opaque:' . $nonce->opaque;
317 $self->store_digest_authorization_nonce( $c, $key, $nonce );
319 return _join_auth_header_parts( Digest =>
320 $self->_build_auth_header_common($c, $opts),
321 map { sprintf '%s="%s"', $_, $nonce->$_ } qw(
330 sub _digest_auth_nonce {
331 my ( $self, $c, $opts ) = @_;
333 my $package = __PACKAGE__ . '::Nonce';
335 my $nonce = $package->new;
337 if ( my $algorithm = $opts->{algorithm} || $self->algorithm) {
338 $nonce->algorithm( $algorithm );
344 sub _join_auth_header_parts {
345 my ( $type, @parts ) = @_;
346 return "$type " . join(", ", @parts );
349 sub get_digest_authorization_nonce {
350 my ( $self, $c, $key ) = @_;
353 return $c->cache->get( $key );
356 sub store_digest_authorization_nonce {
357 my ( $self, $c, $key, $nonce ) = @_;
360 return $c->cache->set( $key, $nonce );
363 package Catalyst::Authentication::Credential::HTTP::Nonce;
366 use base qw[ Class::Accessor::Fast ];
369 our $VERSION = '0.02';
371 __PACKAGE__->mk_accessors(qw[ nonce nonce_count qop opaque algorithm ]);
375 my $self = $class->SUPER::new(@_);
377 $self->nonce( Data::UUID->new->create_b64 );
378 $self->opaque( Data::UUID->new->create_b64 );
379 $self->qop('auth,auth-int');
380 $self->nonce_count('0x0');
381 $self->algorithm('MD5');
394 Catalyst::Authentication::Credential::HTTP - HTTP Basic and Digest authentication
403 __PACKAGE__->config( authentication => {
404 default_realm => 'example',
409 type => 'any', # or 'digest' or 'basic'
410 password_type => 'clear',
411 password_field => 'password'
416 Mufasa => { password => "Circle Of Life", },
424 my ( $self, $c ) = @_;
426 $c->authenticate({ realm => "example" });
427 # either user gets authenticated or 401 is sent
428 # Note that the authentication realm sent to the client (in the
429 # RFC 2617 sense) is overridden here, but this *does not*
430 # effect the Catalyst::Authentication::Realm used for
431 # authentication - to do that, you need
432 # $c->authenticate({}, 'otherrealm')
437 sub always_auth : Local {
438 my ( $self, $c ) = @_;
440 # Force authorization headers onto the response so that the user
441 # is asked again for authentication, even if they successfully
443 my $realm = $c->get_auth_realm('example');
444 $realm->credential->authorization_required_response($c, $realm);
448 __PACKAGE__->deny_access_unless("/path", sub { $_[0]->authenticate });
452 This module lets you use HTTP authentication with
453 L<Catalyst::Plugin::Authentication>. Both basic and digest authentication
454 are currently supported.
456 When authentication is required, this module sets a status of 401, and
457 the body of the response to 'Authorization required.'. To override
458 this and set your own content, check for the C<< $c->res->status ==
459 401 >> in your C<end> action, and change the body accordingly.
467 A nonce is a one-time value sent with each digest authentication
468 request header. The value must always be unique, so per default the
469 last value of the nonce is kept using L<Catalyst::Plugin::Cache>. To
470 change this behaviour, override the
471 C<store_digest_authorization_nonce> and
472 C<get_digest_authorization_nonce> methods as shown below.
480 =item new $config, $c, $realm
486 Validates that $config is ok.
488 =item authenticate $c, $realm, \%auth_info
490 Tries to authenticate the user, and if that fails calls
491 C<authorization_required_response> and detaches the current action call stack.
493 Looks inside C<< $c->request->headers >> and processes the digest and basic
494 (badly named) authorization header.
496 This will only try the methods set in the configuration. First digest, then basic.
498 The %auth_info hash can contain a number of keys which control the authentication behaviour:
504 Sets the HTTP authentication realm presented to the client. Note this does not alter the
505 Catalyst::Authentication::Realm object used for the authentication.
509 Array reference to domains used to build the authorization headers.
511 This list of domains defines the protection space. If a domain URI is an
512 absolute path (starts with /), it is relative to the root URL of the server being accessed.
513 An absolute URI in this list may refer to a different server than the one being accessed.
515 The client will use this list to determine the set of URIs for which the same authentication
516 information may be sent.
518 If this is omitted or its value is empty, the client will assume that the
519 protection space consists of all URIs on the responding server.
521 Therefore, if your application is not hosted at the root of this domain, and you want to
522 prevent the authentication credentials for this application being sent to any other applications.
523 then you should use the I<use_uri_for> configuration option, and pass a domain of I</>.
527 =item authenticate_basic $c, $realm, \%auth_info
529 Performs HTTP basic authentication.
531 =item authenticate_digest $c, $realm, \%auth_info
533 Performs HTTP digest authentication. Note that the password_type B<must> by I<clear> for
534 digest authentication to succeed, and you must have L<Catalyst::Plugin::Session> in
535 your application as digest authentication needs to store persistent data.
537 Note - if you do not want to store your user passwords as clear text, then it is possible
538 to store instead the MD5 digest in hex of the string '$username:$realm:$password'
540 Takes an additional parameter of I<algorithm>, the possible values of which are 'MD5' (the default)
541 and 'MD5-sess'. For more information about 'MD5-sess', see section 3.2.2.2 in RFC 2617.
543 =item authorization_required_response $c, $realm, \%auth_info
545 Sets C<< $c->response >> to the correct status code, and adds the correct
546 header to demand authentication data from the user agent.
548 Typically used by C<authenticate>, but may be invoked manually.
550 %opts can contain C<domain> and C<algorithm>, which are used to build
553 =item store_digest_authorization_nonce $c, $key, $nonce
555 =item get_digest_authorization_nonce $c, $key
557 Set or get the C<$nonce> object used by the digest auth mode.
559 You may override these methods. By default they will call C<get> and C<set> on
566 All configuration is stored in C<< YourApp->config(authentication => { yourrealm => { credential => { class => 'HTTP', %config } } } >>.
568 This should be a hash, and it can contain the following entries:
574 Can be either C<any> (the default), C<basic> or C<digest>.
576 This controls C<authorization_required_response> and C<authenticate>, but
577 not the "manual" methods.
579 =item authorization_required_message
581 Set this to a string to override the default body content "Authorization required.", or set to undef to suppress body content being generated.
585 The type of password returned by the user object. Same usage as in
586 L<Catalyst::Authentication::Credential::Password|Catalyst::Authentication::Credential::Password/password_type>
590 The name of accessor used to retrieve the value of the password field from the user object. Same usage as in
591 L<Catalyst::Authentication::Credential::Password|Catalyst::Authentication::Credential::Password/password_field>
595 The field name that the user's username is mapped into when finding the user from the realm. Defaults to 'username'.
599 If this configuration key has a true value, then the domain(s) for the authorization header will be
600 run through $c->uri_for(). Use this configuration option if your application is not running at the root
601 of your domain, and you want to ensure that authentication credentials from your application are not shared with
602 other applications on the same server.
608 When using digest authentication, this module will only work together
609 with authentication stores whose User objects have a C<password>
610 method that returns the plain-text password. It will not work together
611 with L<Catalyst::Authentication::Store::Htpasswd>, or
612 L<Catalyst::Authentication::Store::DBIC> stores whose
613 C<password> methods return a hashed or salted version of the password.
617 Updated to current name space and currently maintained
618 by: Tomas Doran C<bobtfish@bobtfish.net>.
624 =item Yuval Kogman, C<nothingmuch@woobling.org>
628 =item Sascha Kiefer C<esskar@cpan.org>
634 Patches contributed by:
644 RFC 2617 (or its successors), L<Catalyst::Plugin::Cache>, L<Catalyst::Plugin::Authentication>
646 =head1 COPYRIGHT & LICENSE
648 Copyright (c) 2005-2008 the aforementioned authors. All rights
649 reserved. This program is free software; you can redistribute
650 it and/or modify it under the same terms as Perl itself.