From 9c361f9b2bf1617fa97d3731a83a926db31e21c9 Mon Sep 17 00:00:00 2001 From: Edmund von der Burg Date: Thu, 3 Mar 2011 15:29:56 +0000 Subject: Allow users to create an account, confirm it and logout --- perllib/FixMyStreet/App/Controller/Auth.pm | 178 +++++++++++++++++++++++++++++ 1 file changed, 178 insertions(+) create mode 100644 perllib/FixMyStreet/App/Controller/Auth.pm (limited to 'perllib/FixMyStreet/App/Controller/Auth.pm') diff --git a/perllib/FixMyStreet/App/Controller/Auth.pm b/perllib/FixMyStreet/App/Controller/Auth.pm new file mode 100644 index 000000000..b21981417 --- /dev/null +++ b/perllib/FixMyStreet/App/Controller/Auth.pm @@ -0,0 +1,178 @@ +package FixMyStreet::App::Controller::Auth; +use Moose; +use namespace::autoclean; + +BEGIN { extends 'Catalyst::Controller'; } + +use Email::Valid; +use Net::Domain::TLD; +use mySociety::AuthToken; +use Digest::SHA1 qw(sha1_hex); + +=head1 NAME + +FixMyStreet::App::Controller::Auth - Catalyst Controller + +=head1 DESCRIPTION + +Controller for all the authentication related pages - create account, login, +logout. + +=head1 METHODS + +=head2 index + +Present the user with a login / create account page. + +=cut + +sub general : Path : Args(0) { + my ( $self, $c ) = @_; + my $req = $c->req; + + # all done unless we have a form posted to us + return unless $req->method eq 'POST'; + + # check that the email is valid - otherwise flag an error + my $raw_email = $req->param('email') || ''; + my $email_checker = Email::Valid->new( + -mxcheck => 1, + -tldcheck => 1, + -fqdn => 1, + ); + + if ( my $good_email = $email_checker->address($raw_email) ) { + $c->stash->{email} = $good_email; + } + else { + $c->stash->{email} = $raw_email; + $c->stash->{email_error} = + $raw_email ? $email_checker->details : 'missing'; + return; + } + + # decide which action to take + $c->detach('create_account') if $req->param('create_account'); + + # hmm - should not get this far. 404 so that user knows there is a problem + # rather than it silently not working. + $c->detach('/page_not_found'); + +} + +=head2 create_account + +Create an account for the user, send them an email with confirm link and log +them in straight away. If the email address already has an account send them an +email with a password reset link (slightly leaks privacy information but +required to allow instant logins). + +=cut + +sub create_account : Private { + my ( $self, $c ) = @_; + my $email = $c->stash->{email}; + + # get account from the database + my $account = $c->model('DB::User')->find_or_new( { email => $email } ); + + # Deal with existing accounts by treating it like a password reset link + if ( $account->in_storage ) { + $c->stash->{tried_to_create_account} = 1; + $c->detach('email_reset'); + } + + # we have a new account + my $password = mySociety::AuthToken::random_token(); + $account->password( sha1_hex($password) ); + $account->insert; # save to database + + # log the user in, send them an email and redirect to the welcome page + $c->authenticate( { email => $email, password => $password } ); + $c->send_email( 'auth_new_account_welcome', { to => $email } ); + $c->res->redirect( $c->uri_for('welcome') ); +} + +=head2 welcome + +Page that new users are redirected to after they have created an account. + +=cut + +sub welcome : Local { + my ( $self, $c ) = @_; + + # FIXME - check that user is logged in! + # pass thru +} + +=head2 confirm + +Confirm that a user can receive email - url is .../confirm/$token + +We don't assume that the user is logged in, but if they are they are logged out +and then logged in as the user they are confirming. The token is destroyed at +the end of the request so it cannot be reused. + +=cut + +sub confirm : Local { + my ( $self, $c, $url_token ) = @_; + + # Use the token to confirm the user and return them. + my $user = $c->model('DB::User')->confirm_user_from_token($url_token); + + # If we did not get a user back then the token was not valid + return if !$user; + + # got a user back which is now confirmed - auth as them + $c->logout(); + $c->authenticate( { email => $user->email }, 'no_password' ); + $c->stash->{user_now_confirmed} = 1; + + # TODO - should we redirect somewhere - perhaps to pending problems? + return; +} + +=head2 logout + +Log the user out. Tell them we've done so. + +=cut + +sub logout : Local { + my ( $self, $c ) = @_; + $c->logout(); +} + +=head2 check_auth + +Utility page - returns a simple message 'OK' and a 200 response if the user is +authenticated and a 'Unauthorized' / 401 reponse if they are not. + +Mainly intended for testing but might also be useful for ajax calls. + +=cut + +sub check_auth : Local { + my ( $self, $c ) = @_; + + # choose the response + my ( $body, $code ) # + = $c->user + ? ( 'OK', 200 ) + : ( 'Unauthorized', 401 ); + + # set the response + $c->res->body($body); + $c->res->code($code); + + # NOTE - really a 401 response should also contain a 'WWW-Authenticate' + # header but we ignore that here. The spec is not keeping up with usage. + + return; +} + +__PACKAGE__->meta->make_immutable; + +1; -- cgit v1.2.3 From 770ffd1d8fb1f023e78df876a29dc36022246692 Mon Sep 17 00:00:00 2001 From: Edmund von der Burg Date: Fri, 4 Mar 2011 11:08:07 +0000 Subject: Completed auth section (main parts at least) --- perllib/FixMyStreet/App/Controller/Auth.pm | 142 ++++++++++++++++++++--------- 1 file changed, 98 insertions(+), 44 deletions(-) (limited to 'perllib/FixMyStreet/App/Controller/Auth.pm') diff --git a/perllib/FixMyStreet/App/Controller/Auth.pm b/perllib/FixMyStreet/App/Controller/Auth.pm index b21981417..2069b3903 100644 --- a/perllib/FixMyStreet/App/Controller/Auth.pm +++ b/perllib/FixMyStreet/App/Controller/Auth.pm @@ -34,7 +34,7 @@ sub general : Path : Args(0) { return unless $req->method eq 'POST'; # check that the email is valid - otherwise flag an error - my $raw_email = $req->param('email') || ''; + my $raw_email = lc( $req->param('email') || '' ); my $email_checker = Email::Valid->new( -mxcheck => 1, -tldcheck => 1, @@ -52,7 +52,8 @@ sub general : Path : Args(0) { } # decide which action to take - $c->detach('create_account') if $req->param('create_account'); + $c->detach('login') if $req->param('login'); + $c->detach('email_login') if $req->param('email_login'); # hmm - should not get this far. 404 so that user knows there is a problem # rather than it silently not working. @@ -60,78 +61,131 @@ sub general : Path : Args(0) { } -=head2 create_account +=head2 login -Create an account for the user, send them an email with confirm link and log -them in straight away. If the email address already has an account send them an -email with a password reset link (slightly leaks privacy information but -required to allow instant logins). +Allow the user to legin with a username and a password. =cut -sub create_account : Private { +sub login : Private { my ( $self, $c ) = @_; - my $email = $c->stash->{email}; - # get account from the database - my $account = $c->model('DB::User')->find_or_new( { email => $email } ); + my $email = $c->stash->{email} || ''; + my $password = $c->req->param('password') || ''; - # Deal with existing accounts by treating it like a password reset link - if ( $account->in_storage ) { - $c->stash->{tried_to_create_account} = 1; - $c->detach('email_reset'); - } + # logout just in case + $c->logout(); - # we have a new account - my $password = mySociety::AuthToken::random_token(); - $account->password( sha1_hex($password) ); - $account->insert; # save to database + if ( $c->authenticate( { email => $email, password => $password } ) ) { + $c->res->redirect( $c->uri_for('/my') ); + return; + } - # log the user in, send them an email and redirect to the welcome page - $c->authenticate( { email => $email, password => $password } ); - $c->send_email( 'auth_new_account_welcome', { to => $email } ); - $c->res->redirect( $c->uri_for('welcome') ); + # could not authenticate - show an error + $c->stash->{login_error} = 1; } -=head2 welcome +=head2 email_login -Page that new users are redirected to after they have created an account. +Email the user the details they need to log in. Don't check for an account - if +there isn't one we can create it when they come back with a token (which +contains the email addresss). =cut -sub welcome : Local { +sub email_login : Private { my ( $self, $c ) = @_; + my $email = $c->stash->{email}; - # FIXME - check that user is logged in! - # pass thru -} + my $token_obj = $c->model('DB::Token') # + ->create( + { + scope => 'email_login', + data => { email => $email } + } + ); -=head2 confirm + # log the user in, send them an email and redirect to the welcome page + $c->stash->{token} = $token_obj->token; + $c->send_email( 'login', { to => $email } ); + $c->res->redirect( $c->uri_for('token') ); +} -Confirm that a user can receive email - url is .../confirm/$token +=head2 token -We don't assume that the user is logged in, but if they are they are logged out -and then logged in as the user they are confirming. The token is destroyed at -the end of the request so it cannot be reused. +Handle the 'email_login' tokens. Find the account for the email address +(creating if needed), authenticate the user and delete the token. =cut -sub confirm : Local { +sub token : Local { my ( $self, $c, $url_token ) = @_; - # Use the token to confirm the user and return them. - my $user = $c->model('DB::User')->confirm_user_from_token($url_token); + # check for a token - if none found then return + return unless $url_token; - # If we did not get a user back then the token was not valid - return if !$user; + # retrieve the token or return + my $token_obj = + $c->model('DB::Token') + ->find( { scope => 'email_login', token => $url_token, } ); + + if ( !$token_obj ) { + $c->stash->{token_not_found} = 1; + return; + } - # got a user back which is now confirmed - auth as them + # logout in case we are another user $c->logout(); + + # get the email and scrap the token + my $email = $token_obj->data->{email}; + $token_obj->delete; + + # find or create the user related to the token and delete the token + my $user = $c->model('DB::User')->find_or_create( { email => $email } ); $c->authenticate( { email => $user->email }, 'no_password' ); - $c->stash->{user_now_confirmed} = 1; - # TODO - should we redirect somewhere - perhaps to pending problems? - return; + # send the user to their page + $c->res->redirect( $c->uri_for('/my') ); +} + +=head2 change_password + +Let the user change their password. + +=cut + +sub change_password : Local { + my ( $self, $c ) = @_; + + # FIXME - should be logged in + # FIXME - CSRF check here + # FIXME - minimum criteria for passwords (length, contain number, etc) + + # If not a post then no submission + return unless $c->req->method eq 'POST'; + + # get the passwords + my $new = $c->req->param('new_password') // ''; + my $confirm = $c->req->param('confirm') // ''; + + # check for errors + my $password_error = + !$new && !$confirm ? 'missing' + : $new ne $confirm ? 'mismatch' + : ''; + + if ($password_error) { + $c->stash->{password_error} = $password_error; + $c->stash->{new_password} = $new; + $c->stash->{confirm} = $confirm; + return; + } + + # we should have a usable password - save it to the user + $c->user->obj->update( { password => sha1_hex($new) } ); + $c->stash->{password_changed} = 1; + } =head2 logout -- cgit v1.2.3 From bca2edea2c56fdb3b1d20d42d55ccfd6957900b3 Mon Sep 17 00:00:00 2001 From: Edmund von der Burg Date: Fri, 4 Mar 2011 11:39:54 +0000 Subject: Clean up the flow - only mxcheck emails if we need to (ie not for logging in) --- perllib/FixMyStreet/App/Controller/Auth.pm | 53 ++++++++++++++---------------- 1 file changed, 25 insertions(+), 28 deletions(-) (limited to 'perllib/FixMyStreet/App/Controller/Auth.pm') diff --git a/perllib/FixMyStreet/App/Controller/Auth.pm b/perllib/FixMyStreet/App/Controller/Auth.pm index 2069b3903..912119cd3 100644 --- a/perllib/FixMyStreet/App/Controller/Auth.pm +++ b/perllib/FixMyStreet/App/Controller/Auth.pm @@ -33,31 +33,9 @@ sub general : Path : Args(0) { # all done unless we have a form posted to us return unless $req->method eq 'POST'; - # check that the email is valid - otherwise flag an error - my $raw_email = lc( $req->param('email') || '' ); - my $email_checker = Email::Valid->new( - -mxcheck => 1, - -tldcheck => 1, - -fqdn => 1, - ); - - if ( my $good_email = $email_checker->address($raw_email) ) { - $c->stash->{email} = $good_email; - } - else { - $c->stash->{email} = $raw_email; - $c->stash->{email_error} = - $raw_email ? $email_checker->details : 'missing'; - return; - } - # decide which action to take - $c->detach('login') if $req->param('login'); $c->detach('email_login') if $req->param('email_login'); - - # hmm - should not get this far. 404 so that user knows there is a problem - # rather than it silently not working. - $c->detach('/page_not_found'); + $c->detach('login'); # default } @@ -70,13 +48,16 @@ Allow the user to legin with a username and a password. sub login : Private { my ( $self, $c ) = @_; - my $email = $c->stash->{email} || ''; + my $email = $c->req->param('email') || ''; my $password = $c->req->param('password') || ''; # logout just in case $c->logout(); - if ( $c->authenticate( { email => $email, password => $password } ) ) { + if ( $email + && $password + && $c->authenticate( { email => $email, password => $password } ) ) + { $c->res->redirect( $c->uri_for('/my') ); return; } @@ -95,19 +76,35 @@ contains the email addresss). sub email_login : Private { my ( $self, $c ) = @_; - my $email = $c->stash->{email}; + + # check that the email is valid - otherwise flag an error + my $raw_email = lc( $c->req->param('email') || '' ); + + my $email_checker = Email::Valid->new( + -mxcheck => 1, + -tldcheck => 1, + -fqdn => 1, + ); + + my $good_email = $email_checker->address($raw_email); + if ( !$good_email ) { + $c->stash->{email} = $raw_email; + $c->stash->{email_error} = + $raw_email ? $email_checker->details : 'missing'; + return; + } my $token_obj = $c->model('DB::Token') # ->create( { scope => 'email_login', - data => { email => $email } + data => { email => $good_email } } ); # log the user in, send them an email and redirect to the welcome page $c->stash->{token} = $token_obj->token; - $c->send_email( 'login', { to => $email } ); + $c->send_email( 'login', { to => $good_email } ); $c->res->redirect( $c->uri_for('token') ); } -- cgit v1.2.3 From a126927e7989c9024eb447eff0ef8ec796bb945a Mon Sep 17 00:00:00 2001 From: Edmund von der Burg Date: Wed, 23 Mar 2011 19:25:39 +0000 Subject: use .txt suffix on email templates --- perllib/FixMyStreet/App/Controller/Auth.pm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'perllib/FixMyStreet/App/Controller/Auth.pm') diff --git a/perllib/FixMyStreet/App/Controller/Auth.pm b/perllib/FixMyStreet/App/Controller/Auth.pm index 912119cd3..3d60172cf 100644 --- a/perllib/FixMyStreet/App/Controller/Auth.pm +++ b/perllib/FixMyStreet/App/Controller/Auth.pm @@ -104,7 +104,7 @@ sub email_login : Private { # log the user in, send them an email and redirect to the welcome page $c->stash->{token} = $token_obj->token; - $c->send_email( 'login', { to => $good_email } ); + $c->send_email( 'login.txt', { to => $good_email } ); $c->res->redirect( $c->uri_for('token') ); } -- cgit v1.2.3 From d839ff45d1bbbb65d2e1faac1a6a62a955aabb54 Mon Sep 17 00:00:00 2001 From: Edmund von der Burg Date: Thu, 24 Mar 2011 12:40:49 +0000 Subject: auth related fixes --- perllib/FixMyStreet/App/Controller/Auth.pm | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) (limited to 'perllib/FixMyStreet/App/Controller/Auth.pm') diff --git a/perllib/FixMyStreet/App/Controller/Auth.pm b/perllib/FixMyStreet/App/Controller/Auth.pm index 3d60172cf..16f0b994c 100644 --- a/perllib/FixMyStreet/App/Controller/Auth.pm +++ b/perllib/FixMyStreet/App/Controller/Auth.pm @@ -155,7 +155,12 @@ Let the user change their password. sub change_password : Local { my ( $self, $c ) = @_; - # FIXME - should be logged in + # FIXME - handle not being logged in more elegantly + unless ( $c->user ) { + $c->res->redirect( $c->uri_for('/auth') ); + $c->detach; + } + # FIXME - CSRF check here # FIXME - minimum criteria for passwords (length, contain number, etc) -- cgit v1.2.3 From d0059b5b46bf16d5adbeddffc412699a8c815725 Mon Sep 17 00:00:00 2001 From: Edmund von der Burg Date: Thu, 7 Apr 2011 15:41:59 +0100 Subject: Add the 'remember_me' checkbox on login --- perllib/FixMyStreet/App/Controller/Auth.pm | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) (limited to 'perllib/FixMyStreet/App/Controller/Auth.pm') diff --git a/perllib/FixMyStreet/App/Controller/Auth.pm b/perllib/FixMyStreet/App/Controller/Auth.pm index 16f0b994c..7526c2c25 100644 --- a/perllib/FixMyStreet/App/Controller/Auth.pm +++ b/perllib/FixMyStreet/App/Controller/Auth.pm @@ -48,8 +48,9 @@ Allow the user to legin with a username and a password. sub login : Private { my ( $self, $c ) = @_; - my $email = $c->req->param('email') || ''; - my $password = $c->req->param('password') || ''; + my $email = $c->req->param('email') || ''; + my $password = $c->req->param('password') || ''; + my $remember_me = $c->req->param('remember_me') || 0; # logout just in case $c->logout(); @@ -58,6 +59,11 @@ sub login : Private { && $password && $c->authenticate( { email => $email, password => $password } ) ) { + + # unless user asked to be remembered limit the session to browser + $c->set_session_cookie_expire(0) + unless $remember_me; + $c->res->redirect( $c->uri_for('/my') ); return; } -- cgit v1.2.3