3 package Catalyst::Plugin::Authentication::Credential::HTTP;
4 use base qw/Catalyst::Plugin::Authentication::Credential::Password/;
14 our $VERSION = "0.05";
16 sub authenticate_http {
17 my ( $c, @args ) = @_;
19 return 1 if $c->_is_http_auth_type('digest') && $c->authenticate_digest(@args);
20 return 1 if $c->_is_http_auth_type('basic') && $c->authenticate_basic(@args);
23 sub get_http_auth_store {
24 my ( $c, %opts ) = @_;
25 $opts{store} || $c->config->{authentication}{http}{store};
28 sub authenticate_basic {
29 my ( $c, %opts ) = @_;
31 $c->log->debug('Checking http basic authentication.') if $c->debug;
33 my $headers = $c->req->headers;
35 if ( my ( $username, $password ) = $headers->authorization_basic ) {
39 unless ( $user = $opts{user} ) {
40 if ( my $store = $c->get_http_auth_store(%opts) ) {
41 $user = $store->get_user($username);
47 return $c->login( $user, $password );
53 sub authenticate_digest {
54 my ( $c, %opts ) = @_;
56 $c->log->debug('Checking http digest authentication.') if $c->debug;
58 my $headers = $c->req->headers;
59 my @authorization = $headers->header('Authorization');
60 foreach my $authorization (@authorization) {
61 next unless $authorization =~ m{^Digest};
64 my @key_val = split /=/, $_, 2;
65 $key_val[0] = lc $key_val[0];
66 $key_val[1] =~ s{"}{}g; # remove the quotes
68 } split /,\s?/, substr( $authorization, 7 ); #7 == length "Digest "
70 my $opaque = $res{opaque};
71 my $nonce = $c->_get_digest_authorization_nonce( __PACKAGE__ . '::opaque:' . $opaque );
74 $c->log->debug('Checking authentication parameters.')
77 my $uri = '/' . $c->request->path;
78 my $algorithm = $res{algorithm} || 'MD5';
79 my $nonce_count = '0x' . $res{nc};
81 my $check = $uri eq $res{uri}
82 && ( exists $res{username} )
83 && ( exists $res{qop} )
84 && ( exists $res{cnonce} )
85 && ( exists $res{nc} )
86 && $algorithm eq $nonce->algorithm
87 && hex($nonce_count) > hex( $nonce->nonce_count )
88 && $res{nonce} eq $nonce->nonce; # TODO: set Stale instead
91 $c->log->debug('Digest authentication failed. Bad request.')
93 $c->res->status(400); # bad request
94 die $Catalyst::DETACH;
97 $c->log->debug('Checking authentication response.')
100 my $username = $res{username};
101 my $realm = $res{realm};
104 my $store = $opts{store}
105 || $c->config->{authentication}{http}{store}
106 || $c->default_auth_store;
108 $user = $store->get_user($username) if $store;
110 unless ($user) { # no user, no authentication
111 $c->log->debug('Unknown user: $user.') if $c->debug;
115 # everything looks good, let's check the response
117 # calculate H(A2) as per spec
118 my $ctx = Digest::MD5->new;
119 $ctx->add( join( ':', $c->request->method, $res{uri} ) );
120 if ( $res{qop} eq 'auth-int' ) {
122 Digest::MD5::md5_hex( $c->request->body ); # not sure here
123 $ctx->add( ':', $digest );
125 my $A2_digest = $ctx->hexdigest;
127 # the idea of the for loop:
128 # if we do not want to store the plain password in our user store,
129 # we can store md5_hex("$username:$realm:$password") instead
130 for my $r ( 0 .. 1 ) {
132 # calculate H(A1) as per spec
133 my $A1_digest = $r ? $user->password : do {
134 $ctx = Digest::MD5->new;
135 $ctx->add( join( ':', $username, $realm, $user->password ) );
138 if ( $nonce->algorithm eq 'MD5-sess' ) {
139 $ctx = Digest::MD5->new;
140 $ctx->add( join( ':', $A1_digest, $res{nonce}, $res{cnonce} ) );
141 $A1_digest = $ctx->hexdigest;
144 my $rq_digest = Digest::MD5::md5_hex(
146 $A1_digest, $res{nonce},
147 $res{qop} ? ( $res{nc}, $res{cnonce}, $res{qop} ) : (),
151 $nonce->nonce_count($nonce_count);
152 $c->cache->set( __PACKAGE__ . '::opaque:' . $nonce->opaque,
155 return $c->login( $user, $user->password )
156 if $rq_digest eq $res{response};
166 die "A cache is needed for http digest authentication."
167 unless $c->can('cache');
170 sub _is_http_auth_type {
171 my ( $c, $type ) = @_;
173 my $cfgtype = lc( $c->config->{authentication}{http}{type} || 'any' );
174 return 1 if $cfgtype eq 'any' || $cfgtype eq lc $type;
178 sub authorization_required {
179 my ( $c, @args ) = @_;
181 return 1 if $c->authenticate_http(@args);
183 $c->authorization_required_response(@args);
185 die $Catalyst::DETACH;
188 sub authorization_required_response {
189 my ( $c, %opts ) = @_;
191 $c->res->status(401);
193 # *DONT* short circuit
195 $ok++ if $c->_create_digest_auth_response(\%opts);
196 $ok++ if $c->_create_basic_auth_response(\%opts);
199 die 'Could not build authorization required response. '
200 . 'Did you configure a valid authentication http type: '
201 . 'basic, digest, any';
205 sub _add_authentication_header {
206 my ( $c, $header ) = @_;
207 $c->res->headers->push_header( 'WWW-Authenticate' => $header );
210 sub _create_digest_auth_response {
211 my ( $c, $opts ) = @_;
213 return unless $c->_is_http_auth_type('digest');
215 if ( my $digest = $c->_build_digest_auth_header( $opts ) ) {
216 $c->_add_authentication_header( $digest );
223 sub _create_basic_auth_response {
224 my ( $c, $opts ) = @_;
226 return unless $c->_is_http_auth_type('basic');
228 if ( my $basic = $c->_build_basic_auth_header( $opts ) ) {
229 $c->_add_authentication_header( $basic );
236 sub _build_auth_header_realm {
237 my ( $c, $opts ) = @_;
239 if ( my $realm = $opts->{realm} ) {
240 return 'realm=' . String::Escape::qprintable($realm);
246 sub _build_auth_header_domain {
247 my ( $c, $opts ) = @_;
249 if ( my $domain = $opts->{domain} ) {
250 Catalyst::Excpetion->throw("domain must be an array reference")
251 unless ref($domain) && ref($domain) eq "ARRAY";
254 $c->config->{authentication}{http}{use_uri_for}
255 ? ( map { $c->uri_for($_) } @$domain )
256 : ( map { URI::Escape::uri_escape($_) } @$domain );
258 return qq{domain="@uris"};
264 sub _build_auth_header_common {
265 my ( $c, $opts ) = @_;
268 $c->_build_auth_header_realm($opts),
269 $c->_build_auth_header_domain($opts),
273 sub _build_basic_auth_header {
274 my ( $c, $opts ) = @_;
275 return $c->_join_auth_header_parts( Basic => $c->_build_auth_header_common );
278 sub _build_digest_auth_header {
279 my ( $c, $opts ) = @_;
281 my $nonce = $c->_digest_auth_nonce($opts);
283 my $key = __PACKAGE__ . '::opaque:' . $nonce->opaque;
285 $c->_store_digest_authorization_nonce( $key, $nonce );
287 return $c->_join_auth_header_parts( Digest =>
288 $c->_build_auth_header_common($opts),
289 map { sprintf '%s="%s"', $_, $nonce->$_ } qw(
298 sub _digest_auth_nonce {
299 my ( $c, $opts ) = @_;
301 my $package = __PACKAGE__ . '::Nonce';
303 my $nonce = $package->new;
305 my $algorithm = $opts->{algorithm}
306 || $c->config->{authentication}{http}{algorithm}
307 || $nonce->algorithm;
309 $nonce->algorithm( $algorithm );
314 sub _join_auth_header_parts {
315 my ( $c, $type, @parts ) = @_;
316 return "$type " . join(", ", @parts );
319 sub _get_digest_authorization_nonce {
320 my ( $c, $key ) = @_;
323 $c->cache->get( $key );
326 sub _store_digest_authorization_nonce {
327 my ( $c, $key, $nonce ) = @_;
330 $c->cache->set( $key, $nonce );
333 package Catalyst::Plugin::Authentication::Credential::HTTP::Nonce;
336 use base qw[ Class::Accessor::Fast ];
339 our $VERSION = "0.01";
341 __PACKAGE__->mk_accessors(qw[ nonce nonce_count qop opaque algorithm ]);
345 my $self = $class->SUPER::new(@_);
347 $self->nonce( Data::UUID->new->create_b64 );
348 $self->opaque( Data::UUID->new->create_b64 );
349 $self->qop('auth,auth-int');
350 $self->nonce_count('0x0');
351 $self->algorithm('MD5');
364 Catalyst::Plugin::Authentication::Credential::HTTP - HTTP Basic and Digest authentication
371 Authentication::Store::Moose
372 Authentication::Credential::HTTP
375 __PACKAGE__->config->{authentication}{http}{type} = 'any'; # or 'digest' or 'basic'
376 __PACKAGE__->config->{authentication}{users} = {
377 Mufasa => { password => "Circle Of Life", },
381 my ( $self, $c ) = @_;
383 $c->authorization_required( realm => "foo" ); # named after the status code ;-)
385 # either user gets authenticated or 401 is sent
391 __PACKAGE__->deny_access_unless("/path", sub { $_[0]->authenticate_http });
394 my ( $self, $c ) = @_;
396 $c->authorization_required_response( realm => "foo" );
402 This moduule lets you use HTTP authentication with
403 L<Catalyst::Plugin::Authentication>. Both basic and digest authentication
404 are currently supported.
410 =item authorization_required
412 Tries to C<authenticate_http>, and if that fails calls
413 C<authorization_required_response> and detaches the current action call stack.
415 =item authenticate_http
417 Looks inside C<< $c->request->headers >> and processes the digest and basic
418 (badly named) authorization header.
420 =item authorization_required_response
422 Sets C<< $c->response >> to the correct status code, and adds the correct
423 header to demand authentication data from the user agent.
429 Yuval Kogman, C<nothingmuch@woobling.org>
433 Sascha Kiefer C<esskar@cpan.org>
435 =head1 COPYRIGHT & LICENSE
437 Copyright (c) 2005-2006 the aforementioned authors. All rights
438 reserved. This program is free software; you can redistribute
439 it and/or modify it under the same terms as Perl itself.