mourningdove/cgi-bin/DW/Auth.pm
2026-05-24 01:03:05 +00:00

246 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;