#!/usr/bin/perl # # DW::Auth # # Alternate authentication styles # # Authors: # Andrea Nall # Afuna # # 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 =item Afuna =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;