diff --git a/lib/SyTest/Identity/Server.pm b/lib/SyTest/Identity/Server.pm index ede349def..15ed74fe1 100644 --- a/lib/SyTest/Identity/Server.pm +++ b/lib/SyTest/Identity/Server.pm @@ -7,11 +7,13 @@ use base qw( Net::Async::HTTP::Server ); use Crypt::NaCl::Sodium; use List::Util qw( any ); +use List::UtilsBy qw( extract_first_by ); use Protocol::Matrix qw( encode_base64_unpadded sign_json ); use MIME::Base64 qw ( encode_base64url ); use SyTest::HTTPServer::Request; use HTTP::Response; use Digest::SHA qw( sha256 ); +use Struct::Dumb qw( struct ); my $crypto_sign = Crypt::NaCl::Sodium->sign; @@ -22,6 +24,8 @@ my $next_token = 0; # `access_token` parameter my $ID_ACCESS_TOKEN = "swordfish"; +struct Awaiter => [qw( future path_match )]; + sub _init { my $self = shift; @@ -34,6 +38,7 @@ sub _init $self->{bindings} = {}; $self->{invites} = {}; + $self->{awaiters} = []; # String for peppering hashed lookup requests $self->{lookup_pepper} = "matrixrocks"; @@ -68,6 +73,34 @@ sub rotate_keys }; } +=head2 await_request + + $server->await_request( + path => "/_matrix/identity/...", + )->then( sub { + my ( $req ) = @_; + }); + +Wait for a request to arrive at the identity server. + +=cut + +sub await_request +{ + my ( $self, %params ) = @_; + + my $path_match = $params{path}; + my $f = $self->loop->new_future; + my $awaiter = Awaiter( $f, $path_match ); + push @{$self->{awaiters}}, $awaiter; + + $f->on_cancel( sub { + extract_first_by { $_ == $awaiter } @{$self->{awaiters}}; + }); + + return $f; +} + =head2 on_request Handles incoming HTTP requests to this server. @@ -80,6 +113,17 @@ sub on_request my ( $req ) = @_; my $path = $req->path; + + # first see if we've got an awaiter for this request + my $awaiter = extract_first_by { + $_->path_match eq $path + } @{$self->{awaiters}}; + + if( $awaiter ) { + $awaiter->future->done( $req ); + return; + } + my $key_name; if( diff --git a/tests/12login/01threepid-and-password.pl b/tests/12login/01threepid-and-password.pl index b5313e026..a639cbfb9 100644 --- a/tests/12login/01threepid-and-password.pl +++ b/tests/12login/01threepid-and-password.pl @@ -1,21 +1,116 @@ my $password = "my secure password"; +=head2 validate_email_for_user -test "Can login with 3pid and password using m.login.password", - requires => [ local_user_fixture( password => $password ), id_server_fixture() ], + validate_email_for_user( + $user, $address, $id_server, + )->then( sub { + my ( $sid, $client_secret ) = @_; + }); - check => sub { - my ( $user, $id_server ) = @_; +Runs through the `/r0/account/3pid/email/requestToken` flow for verifying +that an email address belongs to the user. Doesn't add the address to the +account. - my $http = $user->http; +Returns the session id and client secret which can then be used for binding +the address. - my $medium = "email"; - my $address = 'bob@example.com'; - my $client_secret = "a client secret"; - my $id_access_token = $id_server->get_access_token(); +=cut + +sub validate_email_for_user { + my ( $user, $address, $id_server ) = @_; + + # fixme: synapse screws up the escaping of non-alpha chars. + my $client_secret = join "", map { chr 65 + rand 26 } 1 .. 20; + + my $sid; + + return Future->needs_all( + do_request_json_for( + $user, + method => "POST", + uri => "/r0/account/3pid/email/requestToken", + content => { + client_secret => $client_secret, + email => $address, + send_attempt => 1, + id_server => $id_server->name, + id_access_token => $id_server->get_access_token(), + }, + )->then( sub { + my ( $resp ) = @_; + log_if_fail "requestToken response", $resp; + + $sid = $resp->{sid}; + Future->done; + }), + + # we now expect a callout to our test ID server. + await_id_validation( $id_server, $address ), + )->then( sub { + Future->done( $sid, $client_secret ); + }); +} + +push our @EXPORT, qw( validate_email_for_user ); + +# wait for a call to /requestToken on the test IS, and act as if the +# email has been validated. +sub await_id_validation { + my ( $id_server, $address ) = @_; + + $id_server->await_request( + path=>"/_matrix/identity/api/v1/validate/email/requestToken", + )->then( sub { + my ( $req ) = @_; + my $body = $req->body_from_json; + + log_if_fail "ID server /requestToken request", $body; + assert_eq( $body->{email}, $address ); + my $sid = $id_server->validate_identity( 'email', $address, $body->{client_secret} ); + $req->respond_json({ + sid => $sid, + }); + Future->done(); + }); +} + +=head2 add_email_for_user + + add_email_for_user( + $user, $address, $id_server, %params + ); - my $sid = $id_server->validate_identity( $medium, $address, $client_secret ); +Add the given email address to the homeserver account, including the +verfication steps. +C<%params> may include: + +=over + +=item bind => SCALAR + +If truthy, we will ask the server to ask the IS to bind the email +address. False by default. + +=back + +=cut + +sub add_email_for_user { + my ( $user, $address, $id_server, %params ) = @_; + + my $bind = $params{bind} // 0; + + my $id_access_token = $id_server->get_access_token(); + + # start by requesting an email validation. + validate_email_for_user( + $user, $address, $id_server, + )->then( sub { + my ( $sid, $client_secret ) = @_; + + # now tell the HS to add the 3pid do_request_json_for( $user, method => "POST", uri => "/r0/account/3pid", @@ -26,16 +121,33 @@ sid => $sid, client_secret => $client_secret, }, - bind => JSON::false, + bind => $bind ? JSON::true : JSON::false, }, - )->then( sub { + ); + }); +} + +push @EXPORT, qw( add_email_for_user ); + +test "Can login with 3pid and password using m.login.password", + requires => [ local_user_fixture( password => $password ), id_server_fixture() ], + + check => sub { + my ( $user, $id_server ) = @_; + + my $http = $user->http; + + my $address = 'bob@example.com'; + + add_email_for_user( $user, $address, $id_server ) + ->then( sub { $http->do_request_json( method => "POST", uri => "/r0/login", content => { type => "m.login.password", - medium => $medium, + medium => 'email', address => $address, password => $password, } diff --git a/tests/54identity.pl b/tests/54identity.pl index 943c245a9..c528b35f1 100644 --- a/tests/54identity.pl +++ b/tests/54identity.pl @@ -5,24 +5,10 @@ my ( $http, $user, $id_server ) = @_; my $medium = "email"; - my $address = 'bob@example.com'; - my $client_secret = "a client secret"; - my $id_access_token = $id_server->get_access_token; + my $address = 'bob1@example.com'; - my $sid = $id_server->validate_identity( $medium, $address, $client_secret ); - - do_request_json_for( $user, - method => "POST", - uri => "/r0/account/3pid", - content => { - three_pid_creds => { - id_server => $id_server->name, - id_access_token => $id_access_token, - sid => $sid, - client_secret => $client_secret, - }, - bind => JSON::true, - }, + add_email_for_user( + $user, $address, $id_server, bind => 1, )->then( sub { my $res = $id_server->lookup_identity( $medium, $address ); @@ -40,24 +26,10 @@ my ( $http, $user, $id_server ) = @_; my $medium = "email"; - my $address = 'bob@example.com'; - my $client_secret = "a client secret"; - my $id_access_token = $id_server->get_access_token; + my $address = 'bob2@example.com'; - my $sid = $id_server->validate_identity( $medium, $address, $client_secret ); - - do_request_json_for( $user, - method => "POST", - uri => "/r0/account/3pid", - content => { - three_pid_creds => { - id_server => $id_server->name, - id_access_token => $id_access_token, - sid => $sid, - client_secret => $client_secret, - }, - bind => JSON::true, - }, + add_email_for_user( + $user, $address, $id_server, bind => 1, )->then( sub { my $res = $id_server->lookup_identity( $medium, $address ); assert_eq( $res, $user->user_id ); @@ -86,7 +58,7 @@ my ( $http, $user, $id_server ) = @_; my $medium = "email"; - my $address = 'bob@example.com'; + my $address = 'bob3@example.com'; # Bind the 3PID out of band of the homeserver $id_server->bind_identity( undef, $medium, $address, $user->user_id ); @@ -117,24 +89,10 @@ my ( $http, $user, $id_server ) = @_; my $medium = "email"; - my $address = 'bob@example.com'; - my $client_secret = "a client secret"; - my $id_access_token = $id_server->get_access_token; + my $address = 'bob4@example.com'; - my $sid = $id_server->validate_identity( $medium, $address, $client_secret ); - - do_request_json_for( $user, - method => "POST", - uri => "/r0/account/3pid", - content => { - three_pid_creds => { - id_server => $id_server->name, - id_access_token => $id_access_token, - sid => $sid, - client_secret => $client_secret, - }, - bind => JSON::true, - }, + add_email_for_user( + $user, $address, $id_server, bind => 1, )->then( sub { my $res = $id_server->lookup_identity( $medium, $address ); assert_eq( $res, $user->user_id ); @@ -157,21 +115,9 @@ my $medium = "email"; my $address = 'bobby@example.com'; - my $client_secret = "53kr3t"; - my $sid = $id_server->validate_identity( $medium, $address, $client_secret ); - - do_request_json_for( $user, - method => "POST", - uri => "/r0/account/3pid", - content => { - three_pid_creds => { - id_server => $id_server->name, - sid => $sid, - client_secret => $client_secret, - }, - bind => JSON::true, - }, + add_email_for_user( + $user, $address, $id_server, bind => 1, )->then( sub { my $res = $id_server->lookup_identity( $medium, $address ); assert_eq( $res, $user->user_id ); @@ -201,22 +147,10 @@ my ( $http, $user, $id_server ) = @_; my $medium = "email"; - my $address = 'bobby@example.com'; - my $client_secret = "53kr3t"; - - my $sid = $id_server->validate_identity( $medium, $address, $client_secret ); + my $address = 'bobby2@example.com'; - do_request_json_for( $user, - method => "POST", - uri => "/r0/account/3pid", - content => { - three_pid_creds => { - id_server => $id_server->name, - sid => $sid, - client_secret => $client_secret, - }, - bind => JSON::true, - }, + add_email_for_user( + $user, $address, $id_server, bind => 1, )->then( sub { my $res = $id_server->lookup_identity( $medium, $address ); assert_eq( $res, $user->user_id );