1 package Catalyst::Authentication::Credential::HTTP;
2 use base qw/Catalyst::Authentication::Credential::Password/;
13 __PACKAGE__->mk_accessors(qw/_config realm/);
16 our $VERSION = "1.007";
19 my ($class, $config, $app, $realm) = @_;
21 $config->{username_field} ||= 'username';
22 my $self = { _config => $config, _debug => $app->debug };
33 my $type = $self->_config->{'type'} ||= 'any';
35 if (!grep /$type/, ('basic', 'digest', 'any')) {
36 Catalyst::Exception->throw(__PACKAGE__ . " used with unsupported authentication type: " . $type);
41 my ( $self, $c, $realm, $auth_info ) = @_;
44 $auth = $self->authenticate_digest($c, $realm, $auth_info) if $self->_is_http_auth_type('digest');
45 return $auth if $auth;
47 $auth = $self->authenticate_basic($c, $realm, $auth_info) if $self->_is_http_auth_type('basic');
48 return $auth if $auth;
50 $self->authorization_required_response($c, $realm, $auth_info);
51 die $Catalyst::DETACH;
54 sub authenticate_basic {
55 my ( $self, $c, $realm, $auth_info ) = @_;
57 $c->log->debug('Checking http basic authentication.') if $c->debug;
59 my $headers = $c->req->headers;
61 if ( my ( $username, $password ) = $headers->authorization_basic ) {
62 my $user_obj = $realm->find_user( { $self->_config->{username_field} => $username }, $c);
65 $opts->{$self->_config->{password_field}} = $password
66 if $self->_config->{password_field};
67 if ($self->check_password($user_obj, $opts)) {
68 $c->set_authenticated($user_obj);
73 $c->log->debug("Unable to locate user matching user info provided") if $c->debug;
81 sub authenticate_digest {
82 my ( $self, $c, $realm, $auth_info ) = @_;
84 $c->log->debug('Checking http digest authentication.') if $c->debug;
86 my $headers = $c->req->headers;
87 my @authorization = $headers->header('Authorization');
88 foreach my $authorization (@authorization) {
89 next unless $authorization =~ m{^Digest};
91 my @key_val = split /=/, $_, 2;
92 $key_val[0] = lc $key_val[0];
93 $key_val[1] =~ s{"}{}g; # remove the quotes
95 } split /,\s?/, substr( $authorization, 7 ); #7 == length "Digest "
97 my $opaque = $res{opaque};
98 my $nonce = $self->get_digest_authorization_nonce( $c, __PACKAGE__ . '::opaque:' . $opaque );
101 $c->log->debug('Checking authentication parameters.')
104 my $uri = $c->request->uri->path_query;
105 my $algorithm = $res{algorithm} || 'MD5';
106 my $nonce_count = '0x' . $res{nc};
108 my $check = $uri eq $res{uri}
109 && ( exists $res{username} )
110 && ( exists $res{qop} )
111 && ( exists $res{cnonce} )
112 && ( exists $res{nc} )
113 && $algorithm eq $nonce->algorithm
114 && hex($nonce_count) > hex( $nonce->nonce_count )
115 && $res{nonce} eq $nonce->nonce; # TODO: set Stale instead
118 $c->log->debug('Digest authentication failed. Bad request.')
120 $c->res->status(400); # bad request
121 Carp::confess $Catalyst::DETACH;
124 $c->log->debug('Checking authentication response.')
127 my $username = $res{username};
131 unless ( $user = $auth_info->{user} ) {
132 $user = $realm->find_user( { $self->_config->{username_field} => $username }, $c);
134 unless ($user) { # no user, no authentication
135 $c->log->debug("Unable to locate user matching user info provided") if $c->debug;
139 # everything looks good, let's check the response
140 # calculate H(A2) as per spec
141 my $ctx = Digest::MD5->new;
142 $ctx->add( join( ':', $c->request->method, $res{uri} ) );
143 if ( $res{qop} eq 'auth-int' ) {
145 Digest::MD5::md5_hex( $c->request->body ); # not sure here
146 $ctx->add( ':', $digest );
148 my $A2_digest = $ctx->hexdigest;
150 # the idea of the for loop:
151 # if we do not want to store the plain password in our user store,
152 # we can store md5_hex("$username:$realm:$password") instead
153 my $password_field = $self->_config->{password_field};
154 for my $r ( 0 .. 1 ) {
155 # calculate H(A1) as per spec
156 my $A1_digest = $r ? $user->$password_field() : do {
157 $ctx = Digest::MD5->new;
158 $ctx->add( join( ':', $username, $realm->name, $user->$password_field() ) );
161 if ( $nonce->algorithm eq 'MD5-sess' ) {
162 $ctx = Digest::MD5->new;
163 $ctx->add( join( ':', $A1_digest, $res{nonce}, $res{cnonce} ) );
164 $A1_digest = $ctx->hexdigest;
167 my $digest_in = join( ':',
168 $A1_digest, $res{nonce},
169 $res{qop} ? ( $res{nc}, $res{cnonce}, $res{qop} ) : (),
171 my $rq_digest = Digest::MD5::md5_hex($digest_in);
172 $nonce->nonce_count($nonce_count);
173 $c->cache->set( __PACKAGE__ . '::opaque:' . $nonce->opaque,
175 if ($rq_digest eq $res{response}) {
176 $c->set_authenticated($user);
187 die "A cache is needed for http digest authentication."
188 unless $c->can('cache');
192 sub _is_http_auth_type {
193 my ( $self, $type ) = @_;
194 my $cfgtype = lc( $self->_config->{'type'} || 'any' );
195 return 1 if $cfgtype eq 'any' || $cfgtype eq lc $type;
199 sub authorization_required_response {
200 my ( $self, $c, $realm, $auth_info ) = @_;
202 $c->res->status(401);
203 $c->res->content_type('text/plain');
204 if (exists $self->_config->{authorization_required_message}) {
205 # If you set the key to undef, don't stamp on the body.
206 $c->res->body($self->_config->{authorization_required_message})
207 if defined $c->res->body($self->_config->{authorization_required_message});
210 $c->res->body('Authorization required.');
213 # *DONT* short circuit
215 $ok++ if $self->_create_digest_auth_response($c, $auth_info);
216 $ok++ if $self->_create_basic_auth_response($c, $auth_info);
219 die 'Could not build authorization required response. '
220 . 'Did you configure a valid authentication http type: '
221 . 'basic, digest, any';
226 sub _add_authentication_header {
227 my ( $c, $header ) = @_;
228 $c->response->headers->push_header( 'WWW-Authenticate' => $header );
232 sub _create_digest_auth_response {
233 my ( $self, $c, $opts ) = @_;
235 return unless $self->_is_http_auth_type('digest');
237 if ( my $digest = $self->_build_digest_auth_header( $c, $opts ) ) {
238 _add_authentication_header( $c, $digest );
245 sub _create_basic_auth_response {
246 my ( $self, $c, $opts ) = @_;
248 return unless $self->_is_http_auth_type('basic');
250 if ( my $basic = $self->_build_basic_auth_header( $c, $opts ) ) {
251 _add_authentication_header( $c, $basic );
258 sub _build_auth_header_realm {
259 my ( $self, $c, $opts ) = @_;
260 if ( my $realm_name = String::Escape::qprintable($opts->{realm} ? $opts->{realm} : $self->realm->name) ) {
261 $realm_name = qq{"$realm_name"} unless $realm_name =~ /^"/;
262 return 'realm=' . $realm_name;
267 sub _build_auth_header_domain {
268 my ( $self, $c, $opts ) = @_;
269 if ( my $domain = $opts->{domain} ) {
270 Catalyst::Exception->throw("domain must be an array reference")
271 unless ref($domain) && ref($domain) eq "ARRAY";
274 $self->_config->{use_uri_for}
275 ? ( map { $c->uri_for($_) } @$domain )
276 : ( map { URI::Escape::uri_escape($_) } @$domain );
278 return qq{domain="@uris"};
283 sub _build_auth_header_common {
284 my ( $self, $c, $opts ) = @_;
286 $self->_build_auth_header_realm($c, $opts),
287 $self->_build_auth_header_domain($c, $opts),
291 sub _build_basic_auth_header {
292 my ( $self, $c, $opts ) = @_;
293 return _join_auth_header_parts( Basic => $self->_build_auth_header_common( $c, $opts ) );
296 sub _build_digest_auth_header {
297 my ( $self, $c, $opts ) = @_;
299 my $nonce = $self->_digest_auth_nonce($c, $opts);
301 my $key = __PACKAGE__ . '::opaque:' . $nonce->opaque;
303 $self->store_digest_authorization_nonce( $c, $key, $nonce );
305 return _join_auth_header_parts( Digest =>
306 $self->_build_auth_header_common($c, $opts),
307 map { sprintf '%s="%s"', $_, $nonce->$_ } qw(
316 sub _digest_auth_nonce {
317 my ( $self, $c, $opts ) = @_;
319 my $package = __PACKAGE__ . '::Nonce';
321 my $nonce = $package->new;
323 if ( my $algorithm = $opts->{algorithm} || $self->_config->{algorithm}) {
324 $nonce->algorithm( $algorithm );
330 sub _join_auth_header_parts {
331 my ( $type, @parts ) = @_;
332 return "$type " . join(", ", @parts );
335 sub get_digest_authorization_nonce {
336 my ( $self, $c, $key ) = @_;
339 return $c->cache->get( $key );
342 sub store_digest_authorization_nonce {
343 my ( $self, $c, $key, $nonce ) = @_;
346 return $c->cache->set( $key, $nonce );
349 package Catalyst::Authentication::Credential::HTTP::Nonce;
352 use base qw[ Class::Accessor::Fast ];
355 our $VERSION = '0.02';
357 __PACKAGE__->mk_accessors(qw[ nonce nonce_count qop opaque algorithm ]);
361 my $self = $class->SUPER::new(@_);
363 $self->nonce( Data::UUID->new->create_b64 );
364 $self->opaque( Data::UUID->new->create_b64 );
365 $self->qop('auth,auth-int');
366 $self->nonce_count('0x0');
367 $self->algorithm('MD5');
380 Catalyst::Authentication::Credential::HTTP - HTTP Basic and Digest authentication
389 __PACKAGE__->config( authentication => {
390 default_realm => 'example',
395 type => 'any', # or 'digest' or 'basic'
396 password_type => 'clear',
397 password_field => 'password'
402 Mufasa => { password => "Circle Of Life", },
410 my ( $self, $c ) = @_;
412 $c->authenticate({ realm => "example" });
413 # either user gets authenticated or 401 is sent
414 # Note that the authentication realm sent to the client (in the
415 # RFC 2617 sense) is overridden here, but this *does not*
416 # effect the Catalyst::Authentication::Realm used for
417 # authentication - to do that, you need
418 # $c->authenticate({}, 'otherrealm')
423 sub always_auth : Local {
424 my ( $self, $c ) = @_;
426 # Force authorization headers onto the response so that the user
427 # is asked again for authentication, even if they successfully
429 my $realm = $c->get_auth_realm('example');
430 $realm->credential->authorization_required_response($c, $realm);
434 __PACKAGE__->deny_access_unless("/path", sub { $_[0]->authenticate });
438 This module lets you use HTTP authentication with
439 L<Catalyst::Plugin::Authentication>. Both basic and digest authentication
440 are currently supported.
442 When authentication is required, this module sets a status of 401, and
443 the body of the response to 'Authorization required.'. To override
444 this and set your own content, check for the C<< $c->res->status ==
445 401 >> in your C<end> action, and change the body accordingly.
453 A nonce is a one-time value sent with each digest authentication
454 request header. The value must always be unique, so per default the
455 last value of the nonce is kept using L<Catalyst::Plugin::Cache>. To
456 change this behaviour, override the
457 C<store_digest_authorization_nonce> and
458 C<get_digest_authorization_nonce> methods as shown below.
466 =item new $config, $c, $realm
472 Validates that $config is ok.
474 =item authenticate $c, $realm, \%auth_info
476 Tries to authenticate the user, and if that fails calls
477 C<authorization_required_response> and detaches the current action call stack.
479 Looks inside C<< $c->request->headers >> and processes the digest and basic
480 (badly named) authorization header.
482 This will only try the methods set in the configuration. First digest, then basic.
484 The %auth_info hash can contain a number of keys which control the authentication behaviour:
490 Sets the HTTP authentication realm presented to the client. Note this does not alter the
491 Catalyst::Authentication::Realm object used for the authentication.
495 Array reference to domains used to build the authorization headers.
497 This list of domains defines the protection space. If a domain URI is an
498 absolute path (starts with /), it is relative to the root URL of the server being accessed.
499 An absolute URI in this list may refer to a different server than the one being accessed.
501 The client will use this list to determine the set of URIs for which the same authentication
502 information may be sent.
504 If this is omitted or its value is empty, the client will assume that the
505 protection space consists of all URIs on the responding server.
507 Therefore, if your application is not hosted at the root of this domain, and you want to
508 prevent the authentication credentials for this application being sent to any other applications.
509 then you should use the I<use_uri_for> configuration option, and pass a domain of I</>.
513 =item authenticate_basic $c, $realm, \%auth_info
515 Performs HTTP basic authentication.
517 =item authenticate_digest $c, $realm, \%auth_info
519 Performs HTTP digest authentication. Note that the password_type B<must> by I<clear> for
520 digest authentication to succeed, and you must have L<Catalyst::Plugin::Session> in
521 your application as digest authentication needs to store persistent data.
523 Note - if you do not want to store your user passwords as clear text, then it is possible
524 to store instead the MD5 digest in hex of the string '$username:$realm:$password'
526 Takes an additional parameter of I<algorithm>, the possible values of which are 'MD5' (the default)
527 and 'MD5-sess'. For more information about 'MD5-sess', see section 3.2.2.2 in RFC 2617.
529 =item authorization_required_response $c, $realm, \%auth_info
531 Sets C<< $c->response >> to the correct status code, and adds the correct
532 header to demand authentication data from the user agent.
534 Typically used by C<authenticate>, but may be invoked manually.
536 %opts can contain C<domain> and C<algorithm>, which are used to build
539 =item store_digest_authorization_nonce $c, $key, $nonce
541 =item get_digest_authorization_nonce $c, $key
543 Set or get the C<$nonce> object used by the digest auth mode.
545 You may override these methods. By default they will call C<get> and C<set> on
552 All configuration is stored in C<< YourApp->config(authentication => { yourrealm => { credential => { class => 'HTTP', %config } } } >>.
554 This should be a hash, and it can contain the following entries:
560 Can be either C<any> (the default), C<basic> or C<digest>.
562 This controls C<authorization_required_response> and C<authenticate>, but
563 not the "manual" methods.
565 =item authorization_required_message
567 Set this to a string to override the default body content "Authorization required.", or set to undef to suppress body content being generated.
571 The type of password returned by the user object. Same usage as in
572 L<Catalyst::Authentication::Credential::Password|Catalyst::Authentication::Credential::Password/password_type>
576 The name of accessor used to retrieve the value of the password field from the user object. Same usage as in
577 L<Catalyst::Authentication::Credential::Password|Catalyst::Authentication::Credential::Password/password_field>
581 The field name that the user's username is mapped into when finding the user from the realm. Defaults to 'username'.
585 If this configuration key has a true value, then the domain(s) for the authorization header will be
586 run through $c->uri_for(). Use this configuration option if your application is not running at the root
587 of your domain, and you want to ensure that authentication credentials from your application are not shared with
588 other applications on the same server.
594 When using digest authentication, this module will only work together
595 with authentication stores whose User objects have a C<password>
596 method that returns the plain-text password. It will not work together
597 with L<Catalyst::Authentication::Store::Htpasswd>, or
598 L<Catalyst::Authentication::Store::DBIC> stores whose
599 C<password> methods return a hashed or salted version of the password.
603 Updated to current name space and currently maintained
604 by: Tomas Doran C<bobtfish@bobtfish.net>.
610 =item Yuval Kogman, C<nothingmuch@woobling.org>
614 =item Sascha Kiefer C<esskar@cpan.org>
620 Patches contributed by:
630 RFC 2617 (or its successors), L<Catalyst::Plugin::Cache>, L<Catalyst::Plugin::Authentication>
632 =head1 COPYRIGHT & LICENSE
634 Copyright (c) 2005-2008 the aforementioned authors. All rights
635 reserved. This program is free software; you can redistribute
636 it and/or modify it under the same terms as Perl itself.