247 lines
5.9 KiB
Perl
247 lines
5.9 KiB
Perl
|
|
#!/usr/bin/perl
|
||
|
|
#
|
||
|
|
# DW::Auth
|
||
|
|
#
|
||
|
|
# Alternate authentication styles
|
||
|
|
#
|
||
|
|
# Authors:
|
||
|
|
# Andrea Nall <anall@andreanall.com>
|
||
|
|
# Afuna <coder.dw@afunamatata.com>
|
||
|
|
#
|
||
|
|
# Copyright (c) 2011 by Dreamwidth Studios, LLC.
|
||
|
|
#
|
||
|
|
# This program is free software; you may redistribute it and/or modify it under
|
||
|
|
# the same terms as Perl itself. For a copy of the license, please reference
|
||
|
|
# 'perldoc perlartistic' or 'perldoc perlgpl'.
|
||
|
|
#
|
||
|
|
package DW::Auth;
|
||
|
|
use strict;
|
||
|
|
|
||
|
|
use Digest::SHA1;
|
||
|
|
use MIME::Base64;
|
||
|
|
|
||
|
|
=head1 NAME
|
||
|
|
|
||
|
|
DW::Auth - Alternate authentication styles.
|
||
|
|
|
||
|
|
=head1 SYNOPSIS
|
||
|
|
|
||
|
|
=cut
|
||
|
|
|
||
|
|
# this returns ( $u, $auth_method, @misc )
|
||
|
|
|
||
|
|
=head1 API
|
||
|
|
|
||
|
|
=head2 C<< $class->authenticate( %opts ) >>
|
||
|
|
|
||
|
|
Authentication methods ( specifiable by opts, in order of preference ):
|
||
|
|
|
||
|
|
All of the following can accept either 1 or a hashref of specific options.
|
||
|
|
|
||
|
|
=over
|
||
|
|
=item B< wsse >
|
||
|
|
WSSE
|
||
|
|
|
||
|
|
Acceptable options:
|
||
|
|
|
||
|
|
=over
|
||
|
|
|
||
|
|
=item B< allow_duplicate_nonce >
|
||
|
|
|
||
|
|
=back
|
||
|
|
|
||
|
|
=item B< digest >
|
||
|
|
HTTP Digest
|
||
|
|
|
||
|
|
=item B< remote >
|
||
|
|
Use the value of LJ::get_remote
|
||
|
|
|
||
|
|
=back
|
||
|
|
|
||
|
|
Additional options:
|
||
|
|
|
||
|
|
=over
|
||
|
|
|
||
|
|
=item B< _keep_remote >
|
||
|
|
Do not call LJ::set_remote with the authenticated user.
|
||
|
|
|
||
|
|
=back
|
||
|
|
|
||
|
|
=cut
|
||
|
|
|
||
|
|
sub authenticate {
|
||
|
|
my ( $class, %opts ) = @_;
|
||
|
|
|
||
|
|
my $r = DW::Request->get;
|
||
|
|
my @fail_subs;
|
||
|
|
|
||
|
|
my $ok = sub {
|
||
|
|
my $u = shift;
|
||
|
|
die 'Called $ok without valid $u' unless $u;
|
||
|
|
LJ::set_remote($u) unless $opts{_keep_remote};
|
||
|
|
return ( $u, @_ );
|
||
|
|
};
|
||
|
|
|
||
|
|
my $fail = sub {
|
||
|
|
LJ::set_remote(undef) unless $opts{_keep_remote};
|
||
|
|
$_->() foreach @fail_subs;
|
||
|
|
return ( undef, @_ );
|
||
|
|
};
|
||
|
|
|
||
|
|
return ( undef, undef ) unless $r;
|
||
|
|
|
||
|
|
if ( $opts{wsse} ) {
|
||
|
|
my $nonce_dup;
|
||
|
|
my $wsse = $r->header_in("X-WSSE");
|
||
|
|
my ( $u, $fail_sub ) = _auth_wsse( $wsse, \$nonce_dup );
|
||
|
|
push @fail_subs, $fail_sub if $fail_sub;
|
||
|
|
my $_opts = {};
|
||
|
|
$_opts = $opts{wsse} if ref $opts{wsse} eq 'HASH';
|
||
|
|
return $fail->( 'wsse', 'wsse_nonce_duplicated' )
|
||
|
|
if $nonce_dup && !$_opts->{allow_duplicate_nonce};
|
||
|
|
return $ok->( $u, 'wsse' ) if $u;
|
||
|
|
}
|
||
|
|
if ( $opts{digest} ) {
|
||
|
|
my ( $u, $fail_sub ) = _auth_basic();
|
||
|
|
push @fail_subs, $fail_sub if $fail_sub;
|
||
|
|
return $ok->( $u, 'digest' ) if $u;
|
||
|
|
}
|
||
|
|
if ( $opts{remote} ) {
|
||
|
|
my $remote = LJ::get_remote();
|
||
|
|
return $ok->( $remote, 'remote' ) if $remote;
|
||
|
|
}
|
||
|
|
|
||
|
|
return $fail->(undef);
|
||
|
|
}
|
||
|
|
|
||
|
|
sub _auth_wsse {
|
||
|
|
my ( $wsse, $nonce_dup ) = @_;
|
||
|
|
|
||
|
|
my $fail = sub {
|
||
|
|
my $reason = shift;
|
||
|
|
|
||
|
|
my $sv = sub {
|
||
|
|
my $r = DW::Request->get;
|
||
|
|
$r->header_out_add( "WWW-Authenticate",
|
||
|
|
"WSSE realm=\"$LJ::SITENAMESHORT\", profile=\"UsernameToken\"" );
|
||
|
|
};
|
||
|
|
return ( undef, $sv );
|
||
|
|
};
|
||
|
|
|
||
|
|
$wsse =~ s/UsernameToken // or return $fail->('no username token');
|
||
|
|
|
||
|
|
# parse credentials into a hash.
|
||
|
|
my %creds;
|
||
|
|
foreach ( split /, /, $wsse ) {
|
||
|
|
my ( $k, $v ) = split '=', $_, 2;
|
||
|
|
$v =~ s/^[\'\"]//;
|
||
|
|
$v =~ s/[\'\"]$//;
|
||
|
|
$v =~ s/=$// if $k =~ /passworddigest/i; # strip base64 newline char
|
||
|
|
$creds{ lc($k) } = $v;
|
||
|
|
}
|
||
|
|
|
||
|
|
# invalid create time? invalid wsse.
|
||
|
|
my $ctime = LJ::ParseFeed::w3cdtf_to_time( $creds{created} )
|
||
|
|
or return $fail->("no created date");
|
||
|
|
|
||
|
|
# prevent replay attacks.
|
||
|
|
$ctime = LJ::mysqldate_to_time( $ctime, 'gmt' );
|
||
|
|
return $fail->("replay time skew") if abs( time() - $ctime ) > 42300;
|
||
|
|
|
||
|
|
my $u = LJ::load_user( LJ::canonical_username( $creds{username} ) )
|
||
|
|
or return $fail->("invalid username [$creds{username}]");
|
||
|
|
|
||
|
|
if ( @LJ::MEMCACHE_SERVERS && ref $nonce_dup ) {
|
||
|
|
$$nonce_dup = 1
|
||
|
|
unless LJ::MemCache::add( "wsse_auth:$creds{username}:$creds{nonce}", 1, 180 );
|
||
|
|
}
|
||
|
|
|
||
|
|
# validate hash
|
||
|
|
my $hash = Digest::SHA1::sha1_base64( $creds{nonce} . $creds{created} . $u->password );
|
||
|
|
|
||
|
|
# Nokia's WSSE implementation is incorrect as of 1.5, and they
|
||
|
|
# base64 encode their nonce *value*. If the initial comparison
|
||
|
|
# fails, we need to try this as well before saying it's invalid.
|
||
|
|
if ( $hash ne $creds{passworddigest} ) {
|
||
|
|
$hash =
|
||
|
|
Digest::SHA1::sha1_base64(
|
||
|
|
MIME::Base64::decode_base64( $creds{nonce} ) . $creds{created} . $u->password );
|
||
|
|
|
||
|
|
if ( $hash ne $creds{passworddigest} ) {
|
||
|
|
LJ::handle_bad_login($u);
|
||
|
|
return $fail->("hash wrong");
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
return $fail->("ip_ratelimiting")
|
||
|
|
if LJ::login_ip_banned($u);
|
||
|
|
|
||
|
|
# If we're here, we're valid.
|
||
|
|
return ( $u, undef );
|
||
|
|
}
|
||
|
|
|
||
|
|
sub _auth_basic {
|
||
|
|
my $r = DW::Request->get;
|
||
|
|
|
||
|
|
my $decline = sub {
|
||
|
|
my $stale = shift;
|
||
|
|
|
||
|
|
my $sv = sub {
|
||
|
|
my $r = DW::Request->get;
|
||
|
|
|
||
|
|
$r->header_out_add( "WWW-Authenticate", "Basic realm=\"$LJ::SITENAMESHORT\"" );
|
||
|
|
};
|
||
|
|
return ( undef, $sv );
|
||
|
|
};
|
||
|
|
|
||
|
|
unless ( $r->header_in("Authorization") ) {
|
||
|
|
return $decline->();
|
||
|
|
}
|
||
|
|
|
||
|
|
my $header = $r->header_in("Authorization");
|
||
|
|
|
||
|
|
my ( $authname, $val ) = split( ' ', $header );
|
||
|
|
|
||
|
|
# sanity checks
|
||
|
|
unless ( $authname eq 'Basic' ) {
|
||
|
|
return $decline->();
|
||
|
|
}
|
||
|
|
$val =~ s/=$//; # strip base64 newline char
|
||
|
|
my $decoded = MIME::Base64::decode_base64($val);
|
||
|
|
|
||
|
|
my ( $username, $password ) = split( ":", $decoded, 2 );
|
||
|
|
|
||
|
|
# the username
|
||
|
|
my $user = LJ::canonical_username($username);
|
||
|
|
my $u = LJ::load_user($user);
|
||
|
|
|
||
|
|
# must be a person and have a password
|
||
|
|
return $decline->() unless $u && $u->is_person;
|
||
|
|
return $decline->()
|
||
|
|
unless $password && $u->check_password($password);
|
||
|
|
|
||
|
|
return ( $u, undef );
|
||
|
|
}
|
||
|
|
|
||
|
|
=head1 AUTHOR
|
||
|
|
|
||
|
|
=over
|
||
|
|
|
||
|
|
=item Andrea Nall <anall@andreanall.com>
|
||
|
|
|
||
|
|
=item Afuna <coder.dw@afunamatata.com>
|
||
|
|
|
||
|
|
=back
|
||
|
|
|
||
|
|
=head1 COPYRIGHT AND LICENSE
|
||
|
|
|
||
|
|
Copyright (c) 2011 by Dreamwidth Studios, LLC.
|
||
|
|
|
||
|
|
This program is free software; you may redistribute it and/or modify it under
|
||
|
|
the same terms as Perl itself. For a copy of the license, please reference
|
||
|
|
'perldoc perlartistic' or 'perldoc perlgpl'.
|
||
|
|
|
||
|
|
=cut
|
||
|
|
|
||
|
|
1;
|