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.006";
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);
64 if ($self->check_password($user_obj, {$self->_config->{password_field} => $password})) {
65 $c->set_authenticated($user_obj);
70 $c->log->debug("Unable to locate user matching user info provided") if $c->debug;
78 sub authenticate_digest {
79 my ( $self, $c, $realm, $auth_info ) = @_;
81 $c->log->debug('Checking http digest authentication.') if $c->debug;
83 my $headers = $c->req->headers;
84 my @authorization = $headers->header('Authorization');
85 foreach my $authorization (@authorization) {
86 next unless $authorization =~ m{^Digest};
88 my @key_val = split /=/, $_, 2;
89 $key_val[0] = lc $key_val[0];
90 $key_val[1] =~ s{"}{}g; # remove the quotes
92 } split /,\s?/, substr( $authorization, 7 ); #7 == length "Digest "
94 my $opaque = $res{opaque};
95 my $nonce = $self->get_digest_authorization_nonce( $c, __PACKAGE__ . '::opaque:' . $opaque );
98 $c->log->debug('Checking authentication parameters.')
101 my $uri = '/' . $c->request->path;
102 my $algorithm = $res{algorithm} || 'MD5';
103 my $nonce_count = '0x' . $res{nc};
105 my $check = $uri eq $res{uri}
106 && ( exists $res{username} )
107 && ( exists $res{qop} )
108 && ( exists $res{cnonce} )
109 && ( exists $res{nc} )
110 && $algorithm eq $nonce->algorithm
111 && hex($nonce_count) > hex( $nonce->nonce_count )
112 && $res{nonce} eq $nonce->nonce; # TODO: set Stale instead
115 $c->log->debug('Digest authentication failed. Bad request.')
117 $c->res->status(400); # bad request
118 Carp::confess $Catalyst::DETACH;
121 $c->log->debug('Checking authentication response.')
124 my $username = $res{username};
128 unless ( $user = $auth_info->{user} ) {
129 $user = $realm->find_user( { $self->_config->{username_field} => $username }, $c);
131 unless ($user) { # no user, no authentication
132 $c->log->debug("Unable to locate user matching user info provided") if $c->debug;
136 # everything looks good, let's check the response
137 # calculate H(A2) as per spec
138 my $ctx = Digest::MD5->new;
139 $ctx->add( join( ':', $c->request->method, $res{uri} ) );
140 if ( $res{qop} eq 'auth-int' ) {
142 Digest::MD5::md5_hex( $c->request->body ); # not sure here
143 $ctx->add( ':', $digest );
145 my $A2_digest = $ctx->hexdigest;
147 # the idea of the for loop:
148 # if we do not want to store the plain password in our user store,
149 # we can store md5_hex("$username:$realm:$password") instead
150 my $password_field = $self->_config->{password_field};
151 for my $r ( 0 .. 1 ) {
152 # calculate H(A1) as per spec
153 my $A1_digest = $r ? $user->$password_field() : do {
154 $ctx = Digest::MD5->new;
155 $ctx->add( join( ':', $username, $realm->name, $user->$password_field() ) );
158 if ( $nonce->algorithm eq 'MD5-sess' ) {
159 $ctx = Digest::MD5->new;
160 $ctx->add( join( ':', $A1_digest, $res{nonce}, $res{cnonce} ) );
161 $A1_digest = $ctx->hexdigest;
164 my $digest_in = join( ':',
165 $A1_digest, $res{nonce},
166 $res{qop} ? ( $res{nc}, $res{cnonce}, $res{qop} ) : (),
168 my $rq_digest = Digest::MD5::md5_hex($digest_in);
169 $nonce->nonce_count($nonce_count);
170 $c->cache->set( __PACKAGE__ . '::opaque:' . $nonce->opaque,
172 if ($rq_digest eq $res{response}) {
173 $c->set_authenticated($user);
184 die "A cache is needed for http digest authentication."
185 unless $c->can('cache');
189 sub _is_http_auth_type {
190 my ( $self, $type ) = @_;
191 my $cfgtype = lc( $self->_config->{'type'} || 'any' );
192 return 1 if $cfgtype eq 'any' || $cfgtype eq lc $type;
196 sub authorization_required_response {
197 my ( $self, $c, $realm, $auth_info ) = @_;
199 $c->res->status(401);
200 $c->res->content_type('text/plain');
201 if (exists $self->_config->{authorization_required_message}) {
202 # If you set the key to undef, don't stamp on the body.
203 $c->res->body($self->_config->{authorization_required_message})
204 if defined $c->res->body($self->_config->{authorization_required_message});
207 $c->res->body('Authorization required.');
210 # *DONT* short circuit
212 $ok++ if $self->_create_digest_auth_response($c, $auth_info);
213 $ok++ if $self->_create_basic_auth_response($c, $auth_info);
216 die 'Could not build authorization required response. '
217 . 'Did you configure a valid authentication http type: '
218 . 'basic, digest, any';
223 sub _add_authentication_header {
224 my ( $c, $header ) = @_;
225 $c->response->headers->push_header( 'WWW-Authenticate' => $header );
229 sub _create_digest_auth_response {
230 my ( $self, $c, $opts ) = @_;
232 return unless $self->_is_http_auth_type('digest');
234 if ( my $digest = $self->_build_digest_auth_header( $c, $opts ) ) {
235 _add_authentication_header( $c, $digest );
242 sub _create_basic_auth_response {
243 my ( $self, $c, $opts ) = @_;
245 return unless $self->_is_http_auth_type('basic');
247 if ( my $basic = $self->_build_basic_auth_header( $c, $opts ) ) {
248 _add_authentication_header( $c, $basic );
255 sub _build_auth_header_realm {
256 my ( $self, $c, $opts ) = @_;
257 if ( my $realm_name = String::Escape::qprintable($opts->{realm} ? $opts->{realm} : $self->realm->name) ) {
258 $realm_name = qq{"$realm_name"} unless $realm_name =~ /^"/;
259 return 'realm=' . $realm_name;
264 sub _build_auth_header_domain {
265 my ( $self, $c, $opts ) = @_;
266 if ( my $domain = $opts->{domain} ) {
267 Catalyst::Exception->throw("domain must be an array reference")
268 unless ref($domain) && ref($domain) eq "ARRAY";
271 $self->_config->{use_uri_for}
272 ? ( map { $c->uri_for($_) } @$domain )
273 : ( map { URI::Escape::uri_escape($_) } @$domain );
275 return qq{domain="@uris"};
280 sub _build_auth_header_common {
281 my ( $self, $c, $opts ) = @_;
283 $self->_build_auth_header_realm($c, $opts),
284 $self->_build_auth_header_domain($c, $opts),
288 sub _build_basic_auth_header {
289 my ( $self, $c, $opts ) = @_;
290 return _join_auth_header_parts( Basic => $self->_build_auth_header_common( $c, $opts ) );
293 sub _build_digest_auth_header {
294 my ( $self, $c, $opts ) = @_;
296 my $nonce = $self->_digest_auth_nonce($c, $opts);
298 my $key = __PACKAGE__ . '::opaque:' . $nonce->opaque;
300 $self->store_digest_authorization_nonce( $c, $key, $nonce );
302 return _join_auth_header_parts( Digest =>
303 $self->_build_auth_header_common($c, $opts),
304 map { sprintf '%s="%s"', $_, $nonce->$_ } qw(
313 sub _digest_auth_nonce {
314 my ( $self, $c, $opts ) = @_;
316 my $package = __PACKAGE__ . '::Nonce';
318 my $nonce = $package->new;
320 if ( my $algorithm = $opts->{algorithm} || $self->_config->{algorithm}) {
321 $nonce->algorithm( $algorithm );
327 sub _join_auth_header_parts {
328 my ( $type, @parts ) = @_;
329 return "$type " . join(", ", @parts );
332 sub get_digest_authorization_nonce {
333 my ( $self, $c, $key ) = @_;
336 return $c->cache->get( $key );
339 sub store_digest_authorization_nonce {
340 my ( $self, $c, $key, $nonce ) = @_;
343 return $c->cache->set( $key, $nonce );
346 package Catalyst::Authentication::Credential::HTTP::Nonce;
349 use base qw[ Class::Accessor::Fast ];
352 our $VERSION = '0.02';
354 __PACKAGE__->mk_accessors(qw[ nonce nonce_count qop opaque algorithm ]);
358 my $self = $class->SUPER::new(@_);
360 $self->nonce( Data::UUID->new->create_b64 );
361 $self->opaque( Data::UUID->new->create_b64 );
362 $self->qop('auth,auth-int');
363 $self->nonce_count('0x0');
364 $self->algorithm('MD5');
377 Catalyst::Authentication::Credential::HTTP - HTTP Basic and Digest authentication
386 __PACKAGE__->config( authentication => {
387 default_realm => 'example',
392 type => 'any', # or 'digest' or 'basic'
393 password_type => 'clear',
394 password_field => 'password'
399 Mufasa => { password => "Circle Of Life", },
407 my ( $self, $c ) = @_;
409 $c->authenticate({ realm => "example" });
410 # either user gets authenticated or 401 is sent
411 # Note that the authentication realm sent to the client (in the
412 # RFC 2617 sense) is overridden here, but this *does not*
413 # effect the Catalyst::Authentication::Realm used for
414 # authentication - to do that, you need
415 # $c->authenticate({}, 'otherrealm')
420 sub always_auth : Local {
421 my ( $self, $c ) = @_;
423 # Force authorization headers onto the response so that the user
424 # is asked again for authentication, even if they successfully
426 my $realm = $c->get_auth_realm('example');
427 $realm->credential->authorization_required_response($c, $realm);
431 __PACKAGE__->deny_access_unless("/path", sub { $_[0]->authenticate });
435 This module lets you use HTTP authentication with
436 L<Catalyst::Plugin::Authentication>. Both basic and digest authentication
437 are currently supported.
439 When authentication is required, this module sets a status of 401, and
440 the body of the response to 'Authorization required.'. To override
441 this and set your own content, check for the C<< $c->res->status ==
442 401 >> in your C<end> action, and change the body accordingly.
450 A nonce is a one-time value sent with each digest authentication
451 request header. The value must always be unique, so per default the
452 last value of the nonce is kept using L<Catalyst::Plugin::Cache>. To
453 change this behaviour, override the
454 C<store_digest_authorization_nonce> and
455 C<get_digest_authorization_nonce> methods as shown below.
463 =item new $config, $c, $realm
469 Validates that $config is ok.
471 =item authenticate $c, $realm, \%auth_info
473 Tries to authenticate the user, and if that fails calls
474 C<authorization_required_response> and detaches the current action call stack.
476 Looks inside C<< $c->request->headers >> and processes the digest and basic
477 (badly named) authorization header.
479 This will only try the methods set in the configuration. First digest, then basic.
481 The %auth_info hash can contain a number of keys which control the authentication behaviour:
487 Sets the HTTP authentication realm presented to the client. Note this does not alter the
488 Catalyst::Authentication::Realm object used for the authentication.
492 Array reference to domains used to build the authorization headers.
494 This list of domains defines the protection space. If a domain URI is an
495 absolute path (starts with /), it is relative to the root URL of the server being accessed.
496 An absolute URI in this list may refer to a different server than the one being accessed.
498 The client will use this list to determine the set of URIs for which the same authentication
499 information may be sent.
501 If this is omitted or its value is empty, the client will assume that the
502 protection space consists of all URIs on the responding server.
504 Therefore, if your application is not hosted at the root of this domain, and you want to
505 prevent the authentication credentials for this application being sent to any other applications.
506 then you should use the I<use_uri_for> configuration option, and pass a domain of I</>.
510 =item authenticate_basic $c, $realm, \%auth_info
512 Performs HTTP basic authentication.
514 =item authenticate_digest $c, $realm, \%auth_info
516 Performs HTTP digest authentication. Note that the password_type B<must> by I<clear> for
517 digest authentication to succeed, and you must have L<Catalyst::Plugin::Session> in
518 your application as digest authentication needs to store persistent data.
520 Note - if you do not want to store your user passwords as clear text, then it is possible
521 to store instead the MD5 digest in hex of the string '$username:$realm:$password'
523 Takes an additional parameter of I<algorithm>, the possible values of which are 'MD5' (the default)
524 and 'MD5-sess'. For more information about 'MD5-sess', see section 3.2.2.2 in RFC 2617.
526 =item authorization_required_response $c, $realm, \%auth_info
528 Sets C<< $c->response >> to the correct status code, and adds the correct
529 header to demand authentication data from the user agent.
531 Typically used by C<authenticate>, but may be invoked manually.
533 %opts can contain C<domain> and C<algorithm>, which are used to build
536 =item store_digest_authorization_nonce $c, $key, $nonce
538 =item get_digest_authorization_nonce $c, $key
540 Set or get the C<$nonce> object used by the digest auth mode.
542 You may override these methods. By default they will call C<get> and C<set> on
549 All configuration is stored in C<< YourApp->config(authentication => { yourrealm => { credential => { class => 'HTTP', %config } } } >>.
551 This should be a hash, and it can contain the following entries:
557 Can be either C<any> (the default), C<basic> or C<digest>.
559 This controls C<authorization_required_response> and C<authenticate>, but
560 not the "manual" methods.
562 =item authorization_required_message
564 Set this to a string to override the default body content "Authorization required.", or set to undef to suppress body content being generated.
568 The type of password returned by the user object. Same usage as in
569 L<Catalyst::Authentication::Credential::Password|Catalyst::Authentication::Credential::Password/password_type>
573 The name of accessor used to retrieve the value of the password field from the user object. Same usage as in
574 L<Catalyst::Authentication::Credential::Password|Catalyst::Authentication::Credential::Password/password_field>
578 The field name that the user's username is mapped into when finding the user from the realm. Defaults to 'username'.
582 If this configuration key has a true value, then the domain(s) for the authorization header will be
583 run through $c->uri_for(). Use this configuration option if your application is not running at the root
584 of your domain, and you want to ensure that authentication credentials from your application are not shared with
585 other applications on the same server.
591 When using digest authentication, this module will only work together
592 with authentication stores whose User objects have a C<password>
593 method that returns the plain-text password. It will not work together
594 with L<Catalyst::Authentication::Store::Htpasswd>, or
595 L<Catalyst::Authentication::Store::DBIC> stores whose
596 C<password> methods return a hashed or salted version of the password.
600 Updated to current name space and currently maintained
601 by: Tomas Doran C<bobtfish@bobtfish.net>.
607 =item Yuval Kogman, C<nothingmuch@woobling.org>
611 =item Sascha Kiefer C<esskar@cpan.org>
617 RFC 2617 (or its successors), L<Catalyst::Plugin::Cache>, L<Catalyst::Plugin::Authentication>
619 =head1 COPYRIGHT & LICENSE
621 Copyright (c) 2005-2008 the aforementioned authors. All rights
622 reserved. This program is free software; you can redistribute
623 it and/or modify it under the same terms as Perl itself.