3 package Catalyst::Plugin::Authentication::Credential::HTTP;
\r
4 use base qw/Catalyst::Plugin::Authentication::Credential::Password/;
\r
9 use String::Escape ();
\r
14 our $VERSION = "0.05";
\r
16 sub authenticate_http {
\r
19 return $c->authenticate_digest || $c->authenticate_basic;
\r
22 sub authenticate_basic {
\r
25 $c->log->debug('Checking http basic authentication.') if $c->debug;
\r
27 my $headers = $c->req->headers;
\r
29 if ( my ( $user, $password ) = $headers->authorization_basic ) {
\r
31 if ( my $store = $c->config->{authentication}{http}{store} ) {
\r
32 $user = $store->get_user($user);
\r
35 return $c->login( $user, $password );
\r
41 sub authenticate_digest {
\r
44 $c->log->debug('Checking http digest authentication.') if $c->debug;
\r
46 my $headers = $c->req->headers;
\r
47 my @authorization = $headers->header('Authorization');
\r
48 foreach my $authorization (@authorization) {
\r
49 next unless $authorization =~ m{^Digest};
\r
54 my @key_val = split /=/, $_, 2;
\r
55 $key_val[0] = lc $key_val[0];
\r
56 $key_val[1] =~ s{"}{}g; # remove the quotes
\r
58 } split /,\s?/, substr( $authorization, 7 ); #7 == length "Digest "
\r
60 my $opaque = $res{opaque};
\r
61 my $nonce = $c->cache->get( __PACKAGE__ . '::opaque:' . $opaque );
\r
64 $c->log->debug('Checking authentication parameters.')
\r
67 my $uri = '/' . $c->request->path;
\r
68 my $algorithm = $res{algorithm} || 'MD5';
\r
69 my $nonce_count = '0x' . $res{nc};
\r
71 my $check = $uri eq $res{uri}
\r
72 && ( exists $res{username} )
\r
73 && ( exists $res{qop} )
\r
74 && ( exists $res{cnonce} )
\r
75 && ( exists $res{nc} )
\r
76 && $algorithm eq $nonce->algorithm
\r
77 && hex($nonce_count) > hex( $nonce->nonce_count )
\r
78 && $res{nonce} eq $nonce->nonce; # TODO: set Stale instead
\r
81 $c->log->debug('Digest authentication failed. Bad request.')
\r
83 $c->res->status(400); # bad request
\r
84 die $Catalyst::DETACH;
\r
87 $c->log->debug('Checking authentication response.')
\r
90 my $username = $res{username};
\r
91 my $realm = $res{realm};
\r
94 my $store = $c->config->{authentication}{http}{store}
\r
95 || $c->default_auth_store;
\r
96 $user = $store->get_user($username) if $store;
\r
97 unless ($user) { # no user, no authentication
\r
98 $c->log->debug('Unknown user: $user.') if $c->debug;
\r
102 # everything looks good, let's check the response
\r
104 # calculate H(A2) as per spec
\r
105 my $ctx = Digest::MD5->new;
\r
106 $ctx->add( join( ':', $c->request->method, $res{uri} ) );
\r
107 if ( $res{qop} eq 'auth-int' ) {
\r
109 Digest::MD5::md5_hex( $c->request->body ); # not sure here
\r
110 $ctx->add( ':', $digest );
\r
112 my $A2_digest = $ctx->hexdigest;
\r
114 # the idea of the for loop:
\r
115 # if we do not want to store the plain password in our user store,
\r
116 # we can store md5_hex("$username:$realm:$password") instead
\r
117 for my $r ( 0 .. 1 ) {
\r
119 # calculate H(A1) as per spec
\r
120 my $A1_digest = $r ? $user->password : do {
\r
121 $ctx = Digest::MD5->new;
\r
122 $ctx->add( join( ':', $username, $realm, $user->password ) );
\r
125 if ( $nonce->algorithm eq 'MD5-sess' ) {
\r
126 $ctx = Digest::MD5->new;
\r
127 $ctx->add( join( ':', $A1_digest, $res{nonce}, $res{cnonce} ) );
\r
128 $A1_digest = $ctx->hexdigest;
\r
131 my $rq_digest = Digest::MD5::md5_hex(
\r
133 $A1_digest, $res{nonce},
\r
134 $res{qop} ? ( $res{nc}, $res{cnonce}, $res{qop} ) : (),
\r
138 $nonce->nonce_count($nonce_count);
\r
139 $c->cache->set( __PACKAGE__ . '::opaque:' . $nonce->opaque,
\r
142 return $c->login( $user, $user->password )
\r
143 if $rq_digest eq $res{response};
\r
153 die "A cache is needed for http digest authentication."
\r
154 unless $c->can('cache');
\r
157 sub _is_auth_type {
\r
158 my ( $c, $type ) = @_;
\r
160 my $cfgtype = lc( $c->config->{authentication}{http}{type} || 'any' );
\r
161 return 1 if $cfgtype eq 'any' || $cfgtype eq lc $type;
\r
165 sub authorization_required {
\r
166 my ( $c, %opts ) = @_;
\r
168 return 1 if $c->_is_auth_type('digest') && $c->authenticate_digest;
\r
169 return 1 if $c->_is_auth_type('basic') && $c->authenticate_basic;
\r
171 $c->authorization_required_response(%opts);
\r
173 die $Catalyst::DETACH;
\r
176 sub authorization_required_response {
\r
177 my ( $c, %opts ) = @_;
\r
179 $c->res->status(401);
\r
181 my ( $digest, $basic );
\r
182 $digest = $c->build_authorization_required_response( \%opts, 'Digest' )
\r
183 if $c->_is_auth_type('digest');
\r
184 $basic = $c->build_authorization_required_response( \%opts, 'Basic' )
\r
185 if $c->_is_auth_type('basic');
\r
187 die 'Could not build authorization required response. '
\r
188 . 'Did you configure a valid authentication http type: '
\r
189 . 'basic, digest, any'
\r
190 unless $digest || $basic;
\r
192 $c->res->headers->push_header( 'WWW-Authenticate' => $digest )
\r
194 $c->res->headers->push_header( 'WWW-Authenticate' => $basic ) if $basic;
\r
197 sub build_authorization_required_response {
\r
198 my ( $c, $opts, $type ) = @_;
\r
201 if ( my $realm = $opts->{realm} ) {
\r
202 push @opts, 'realm=' . String::Escape::qprintable($realm);
\r
205 if ( my $domain = $opts->{domain} ) {
\r
206 Catalyst::Excpetion->throw("domain must be an array reference")
\r
207 unless ref($domain) && ref($domain) eq "ARRAY";
\r
210 $c->config->{authentication}{http}{use_uri_for}
\r
211 ? ( map { $c->uri_for($_) } @$domain )
\r
212 : ( map { URI::Escape::uri_escape($_) } @$domain );
\r
214 push @opts, qq{domain="@uris"};
\r
217 if ( $type eq 'Digest' ) {
\r
218 my $package = __PACKAGE__ . '::Nonce';
\r
219 my $nonce = $package->new;
\r
220 $nonce->algorithm( $c->config->{authentication}{http}{algorithm}
\r
221 || $nonce->algorithm );
\r
223 push @opts, 'qop="' . $nonce->qop . '"';
\r
224 push @opts, 'nonce="' . $nonce->nonce . '"';
\r
225 push @opts, 'opaque="' . $nonce->opaque . '"';
\r
226 push @opts, 'algorithm="' . $nonce->algorithm . '"';
\r
229 $c->cache->set( __PACKAGE__ . '::opaque:' . $nonce->opaque, $nonce );
\r
232 return "$type " . join( ', ', @opts );
\r
235 package Catalyst::Plugin::Authentication::Credential::HTTP::Nonce;
\r
238 use base qw[ Class::Accessor::Fast ];
\r
241 our $VERSION = "0.01";
\r
243 __PACKAGE__->mk_accessors(qw[ nonce nonce_count qop opaque algorithm ]);
\r
247 my $self = $class->SUPER::new(@_);
\r
249 $self->nonce( Data::UUID->new->create_b64 );
\r
250 $self->opaque( Data::UUID->new->create_b64 );
\r
251 $self->qop('auth,auth-int');
\r
252 $self->nonce_count('0x0');
\r
253 $self->algorithm('MD5');
\r
266 Catalyst::Plugin::Authentication::Credential::HTTP - HTTP Basic and Digest authentication
\r
273 Authentication::Store::Moose
\r
274 Authentication::Credential::HTTP
\r
277 __PACKAGE__->config->{authentication}{http}{type} = 'any'; # or 'digest' or 'basic'
278 __PACKAGE__->config->{authentication}{users} = {
279 Mufasa => { password => "Circle Of Life", },
283 my ( $self, $c ) = @_;
\r
285 $c->authorization_required( realm => "foo" ); # named after the status code ;-)
\r
287 # either user gets authenticated or 401 is sent
\r
293 __PACKAGE__->deny_access_unless("/path", sub { $_[0]->authenticate_http });
\r
295 sub end : Private {
\r
296 my ( $self, $c ) = @_;
\r
298 $c->authorization_required_response( realm => "foo" );
\r
304 This moduule lets you use HTTP authentication with
\r
305 L<Catalyst::Plugin::Authentication>. Both basic and digest authentication
\r
306 are currently supported.
\r
312 =item authorization_required
\r
314 Tries to C<authenticate_http>, and if that fails calls
\r
315 C<authorization_required_response> and detaches the current action call stack.
\r
317 =item authenticate_http
\r
319 Looks inside C<< $c->request->headers >> and processes the digest and basic
\r
320 (badly named) authorization header.
\r
322 =item authorization_required_response
\r
324 Sets C<< $c->response >> to the correct status code, and adds the correct
\r
325 header to demand authentication data from the user agent.
\r
331 Yuval Kogman, C<nothingmuch@woobling.org>
\r
335 Sascha Kiefer C<esskar@cpan.org>
\r
337 =head1 COPYRIGHT & LICENSE
\r
339 Copyright (c) 2005-2006 the aforementioned authors. All rights
\r
340 reserved. This program is free software; you can redistribute
\r
341 it and/or modify it under the same terms as Perl itself.
\r