aboutsummaryrefslogtreecommitdiffstats
path: root/perllib/FixMyStreet/App/Controller/Auth
diff options
context:
space:
mode:
Diffstat (limited to 'perllib/FixMyStreet/App/Controller/Auth')
-rw-r--r--perllib/FixMyStreet/App/Controller/Auth/Phone.pm108
-rw-r--r--perllib/FixMyStreet/App/Controller/Auth/Profile.pm173
-rw-r--r--perllib/FixMyStreet/App/Controller/Auth/Social.pm203
3 files changed, 484 insertions, 0 deletions
diff --git a/perllib/FixMyStreet/App/Controller/Auth/Phone.pm b/perllib/FixMyStreet/App/Controller/Auth/Phone.pm
new file mode 100644
index 000000000..8387b9d64
--- /dev/null
+++ b/perllib/FixMyStreet/App/Controller/Auth/Phone.pm
@@ -0,0 +1,108 @@
+package FixMyStreet::App::Controller::Auth::Phone;
+use Moose;
+use namespace::autoclean;
+
+BEGIN { extends 'Catalyst::Controller'; }
+
+use FixMyStreet::SMS;
+
+=head1 NAME
+
+FixMyStreet::App::Controller::Auth::Phone - Catalyst Controller
+
+=head1 DESCRIPTION
+
+Controller for phone SMS based authentication
+
+=head1 METHODS
+
+=head2 code
+
+Handle the submission of a code sent by text to a mobile number.
+
+=cut
+
+sub code : Path('') {
+ my ( $self, $c, $scope, $success_action ) = @_;
+ $c->stash->{template} = 'auth/smsform.html';
+ $scope ||= 'phone_sign_in';
+ $success_action ||= '/auth/process_login';
+
+ my $token = $c->stash->{token} = $c->get_param('token');
+ my $code = $c->get_param('code') || '';
+
+ my $data = $c->stash->{token_data} = $c->forward('/auth/get_token', [ $token, $scope ]) || return;
+
+ $c->stash->{incorrect_code} = 1, return if $data->{code} ne $code;
+
+ $c->detach( $success_action, [ $data, 'phone' ] );
+}
+
+=head2 sign_in
+
+When signing in with a mobile phone number, we are sent here.
+This sends a text to that number with a confirmation code,
+and sets up the token/etc to deal with the response.
+
+=cut
+
+sub sign_in : Private {
+ my ( $self, $c, $parsed ) = @_;
+
+ unless ($parsed->{phone}) {
+ $c->stash->{username_error} = 'other_phone';
+ return;
+ }
+
+ unless ($parsed->{may_be_mobile}) {
+ $c->stash->{username_error} = 'nonmobile';
+ return;
+ }
+
+ (my $number = $parsed->{phone}->format) =~ s/\s+//g;
+
+ if ( FixMyStreet->config('SIGNUPS_DISABLED')
+ && !$c->model('DB::User')->find({ phone => $number })
+ && !$c->stash->{current_user} # don't break the change phone flow
+ ) {
+ $c->stash->{template} = 'auth/token.html';
+ return;
+ }
+
+ my $user_params = {};
+ $user_params->{password} = $c->get_param('password_register')
+ if $c->get_param('password_register');
+ my $user = $c->model('DB::User')->new( $user_params );
+
+ my $token_data = {
+ phone => $number,
+ r => $c->get_param('r'),
+ name => $c->get_param('name'),
+ password => $user->password,
+ };
+ if ($c->stash->{current_user}) {
+ $token_data->{old_user_id} = $c->stash->{current_user}->id;
+ $token_data->{r} = 'auth/change_phone/success';
+ }
+
+ $c->forward('send_token', [ $token_data, 'phone_sign_in', $number ]);
+}
+
+sub send_token : Private {
+ my ( $self, $c, $token_data, $token_scope, $to ) = @_;
+
+ my $result = FixMyStreet::SMS->send_token($token_data, $token_scope, $to);
+ if ($result->{error}) {
+ $c->log->debug("Failure sending text containing code *$result->{random}*");
+ $c->stash->{sms_error} = $result->{error};
+ $c->stash->{username_error} = 'sms_failed';
+ return;
+ }
+ $c->stash->{token} = $result->{token};
+ $c->log->debug("Sending text containing code *$result->{random}*");
+ $c->stash->{template} = 'auth/smsform.html';
+}
+
+__PACKAGE__->meta->make_immutable;
+
+1;
diff --git a/perllib/FixMyStreet/App/Controller/Auth/Profile.pm b/perllib/FixMyStreet/App/Controller/Auth/Profile.pm
new file mode 100644
index 000000000..5e6fe6266
--- /dev/null
+++ b/perllib/FixMyStreet/App/Controller/Auth/Profile.pm
@@ -0,0 +1,173 @@
+package FixMyStreet::App::Controller::Auth::Profile;
+use Moose;
+use namespace::autoclean;
+
+BEGIN { extends 'Catalyst::Controller'; }
+
+use mySociety::AuthToken;
+
+=head1 NAME
+
+FixMyStreet::App::Controller::Auth::Profile - Catalyst Controller
+
+=head1 DESCRIPTION
+
+Controller for all the authentication profile related pages - adding/ changing/
+verifying email, phone, password.
+
+=head1 METHODS
+
+=cut
+
+sub auto {
+ my ( $self, $c ) = @_;
+
+ $c->detach( '/auth/redirect' ) unless $c->user;
+
+ return 1;
+}
+
+=head2 change_password
+
+Let the user change their password.
+
+=cut
+
+sub change_password : Path('/auth/change_password') {
+ my ( $self, $c ) = @_;
+
+ $c->stash->{template} = 'auth/change_password.html';
+
+ $c->forward('/auth/get_csrf_token');
+
+ # If not a post then no submission
+ return unless $c->req->method eq 'POST';
+
+ $c->forward('/auth/check_csrf_token');
+
+ # get the passwords
+ my $new = $c->get_param('new_password') // '';
+ my $confirm = $c->get_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 => $new } );
+ $c->stash->{password_changed} = 1;
+
+}
+
+=head2 change_email
+
+Let the user change their email.
+
+=cut
+
+sub change_email : Path('/auth/change_email') {
+ my ( $self, $c ) = @_;
+
+ $c->stash->{template} = 'auth/change_email.html';
+
+ $c->forward('/auth/get_csrf_token');
+
+ # If not a post then no submission
+ return unless $c->req->method eq 'POST';
+
+ $c->forward('/auth/check_csrf_token');
+ $c->stash->{current_user} = $c->user;
+ $c->stash->{email_template} = 'change_email.txt';
+ $c->forward('/auth/email_sign_in', [ $c->get_param('email') ]);
+}
+
+sub change_phone : Path('/auth/change_phone') {
+ my ( $self, $c ) = @_;
+
+ $c->stash->{template} = 'auth/change_phone.html';
+
+ $c->forward('/auth/get_csrf_token');
+
+ # If not a post then no submission
+ return unless $c->req->method eq 'POST';
+
+ $c->forward('/auth/check_csrf_token');
+ $c->stash->{current_user} = $c->user;
+
+ my $phone = $c->stash->{username} = $c->get_param('username') || '';
+ my $parsed = FixMyStreet::SMS->parse_username($phone);
+
+ # Allow removal of phone number, if we have verified email
+ if (!$phone && !$c->stash->{verifying} && $c->user->email_verified) {
+ $c->user->update({ phone => undef, phone_verified => 0 });
+ $c->flash->{flash_message} = _('You have successfully removed your phone number.');
+ $c->res->redirect('/my');
+ $c->detach;
+ }
+
+ $c->stash->{username_error} = 'missing_phone', return unless $phone;
+ $c->stash->{username_error} = 'other_phone', return unless $parsed->{phone};
+
+ # If we've not used a mobile and we're not specifically verifying,
+ # and phone isn't our only verified way of logging in,
+ # then allow change of number (for e.g. landline).
+ if (!FixMyStreet->config('SMS_AUTHENTICATION') || (!$parsed->{may_be_mobile} && !$c->stash->{verifying} && $c->user->email_verified)) {
+ $c->user->update({ phone => $phone, phone_verified => 0 });
+ $c->flash->{flash_message} = _('You have successfully added your phone number.');
+ $c->res->redirect('/my');
+ $c->detach;
+ }
+
+ $c->forward('/auth/phone/sign_in', [ $parsed ]);
+}
+
+sub verify_item : Path('/auth/verify') : Args(1) {
+ my ( $self, $c, $type ) = @_;
+ $c->stash->{verifying} = 1;
+ $c->detach("change_$type");
+}
+
+sub change_email_success : Path('/auth/change_email/success') {
+ my ( $self, $c ) = @_;
+ $c->flash->{flash_message} = _('You have successfully confirmed your email address.');
+ $c->res->redirect('/my');
+}
+
+sub change_phone_success : Path('/auth/change_phone/success') {
+ my ( $self, $c ) = @_;
+ $c->flash->{flash_message} = _('You have successfully verified your phone number.');
+ $c->res->redirect('/my');
+}
+
+sub generate_token : Path('/auth/generate_token') {
+ my ($self, $c) = @_;
+
+ $c->detach( '/page_error_403_access_denied', [] )
+ unless $c->user and ( $c->user->is_superuser or $c->user->from_body );
+
+ $c->stash->{template} = 'auth/generate_token.html';
+ $c->forward('/auth/get_csrf_token');
+
+ if ($c->req->method eq 'POST') {
+ $c->forward('/auth/check_csrf_token');
+ my $token = mySociety::AuthToken::random_token();
+ $c->user->set_extra_metadata('access_token', $token);
+ $c->user->update();
+ $c->stash->{token_generated} = 1;
+ }
+
+ $c->stash->{existing_token} = $c->user->get_extra_metadata('access_token');
+}
+
+__PACKAGE__->meta->make_immutable;
+
+1;
diff --git a/perllib/FixMyStreet/App/Controller/Auth/Social.pm b/perllib/FixMyStreet/App/Controller/Auth/Social.pm
new file mode 100644
index 000000000..097cac984
--- /dev/null
+++ b/perllib/FixMyStreet/App/Controller/Auth/Social.pm
@@ -0,0 +1,203 @@
+package FixMyStreet::App::Controller::Auth::Social;
+use Moose;
+use namespace::autoclean;
+
+BEGIN { extends 'Catalyst::Controller'; }
+
+use Net::Facebook::Oauth2;
+use Net::Twitter::Lite::WithAPIv1_1;
+
+=head1 NAME
+
+FixMyStreet::App::Controller::Auth::Social - Catalyst Controller
+
+=head1 DESCRIPTION
+
+Controller for the Facebook/Twitter authentication.
+
+=head1 METHODS
+
+=head2 facebook_sign_in
+
+Starts the Facebook authentication sequence.
+
+=cut
+
+sub fb : Private {
+ my ($self, $c) = @_;
+ Net::Facebook::Oauth2->new(
+ application_id => $c->config->{FACEBOOK_APP_ID},
+ application_secret => $c->config->{FACEBOOK_APP_SECRET},
+ callback => $c->uri_for('/auth/Facebook'),
+ );
+}
+
+sub facebook_sign_in : Private {
+ my ( $self, $c ) = @_;
+
+ $c->detach( '/page_error_403_access_denied', [] ) if FixMyStreet->config('SIGNUPS_DISABLED');
+
+ my $fb = $c->forward('fb');
+ my $url = $fb->get_authorization_url(scope => ['email']);
+
+ my %oauth;
+ $oauth{return_url} = $c->get_param('r');
+ $oauth{detach_to} = $c->stash->{detach_to};
+ $oauth{detach_args} = $c->stash->{detach_args};
+ $c->session->{oauth} = \%oauth;
+ $c->res->redirect($url);
+}
+
+=head2 facebook_callback
+
+Handles the Facebook callback request and completes the authentication sequence.
+
+=cut
+
+sub facebook_callback: Path('/auth/Facebook') : Args(0) {
+ my ( $self, $c ) = @_;
+
+ $c->detach('oauth_failure') if $c->get_param('error_code');
+
+ my $fb = $c->forward('fb');
+ my $access_token;
+ eval {
+ $access_token = $fb->get_access_token(code => $c->get_param('code'));
+ };
+ if ($@) {
+ (my $message = $@) =~ s/at [^ ]*Auth.pm.*//;
+ $c->detach('/page_error_500_internal_error', [ $message ]);
+ }
+
+ # save this token in session
+ $c->session->{oauth}{token} = $access_token;
+
+ my $info = $fb->get('https://graph.facebook.com/me?fields=name,email')->as_hash();
+ my $email = lc ($info->{email} || "");
+ $c->forward('oauth_success', [ 'facebook', $info->{id}, $info->{name}, $email ]);
+}
+
+=head2 twitter_sign_in
+
+Starts the Twitter authentication sequence.
+
+=cut
+
+sub tw : Private {
+ my ($self, $c) = @_;
+ Net::Twitter::Lite::WithAPIv1_1->new(
+ ssl => 1,
+ consumer_key => $c->config->{TWITTER_KEY},
+ consumer_secret => $c->config->{TWITTER_SECRET},
+ );
+}
+
+sub twitter_sign_in : Private {
+ my ( $self, $c ) = @_;
+
+ $c->detach( '/page_error_403_access_denied', [] ) if FixMyStreet->config('SIGNUPS_DISABLED');
+
+ my $twitter = $c->forward('tw');
+ my $url = $twitter->get_authentication_url(callback => $c->uri_for('/auth/Twitter'));
+
+ my %oauth;
+ $oauth{return_url} = $c->get_param('r');
+ $oauth{detach_to} = $c->stash->{detach_to};
+ $oauth{detach_args} = $c->stash->{detach_args};
+ $oauth{token} = $twitter->request_token;
+ $oauth{token_secret} = $twitter->request_token_secret;
+ $c->session->{oauth} = \%oauth;
+ $c->res->redirect($url);
+}
+
+=head2 twitter_callback
+
+Handles the Twitter callback request and completes the authentication sequence.
+
+=cut
+
+sub twitter_callback: Path('/auth/Twitter') : Args(0) {
+ my ( $self, $c ) = @_;
+
+ my $request_token = $c->req->param('oauth_token');
+ my $verifier = $c->req->param('oauth_verifier');
+ my $oauth = $c->session->{oauth};
+
+ $c->detach('oauth_failure') if $c->get_param('denied') || $request_token ne $oauth->{token};
+
+ my $twitter = $c->forward('tw');
+ $twitter->request_token($oauth->{token});
+ $twitter->request_token_secret($oauth->{token_secret});
+
+ eval {
+ # request_access_token no longer returns UID or name
+ $twitter->request_access_token(verifier => $verifier);
+ };
+ if ($@) {
+ (my $message = $@) =~ s/at [^ ]*Auth.pm.*//;
+ $c->detach('/page_error_500_internal_error', [ $message ]);
+ }
+
+ my $info = $twitter->verify_credentials();
+ $c->forward('oauth_success', [ 'twitter', $info->{id}, $info->{name} ]);
+}
+
+sub oauth_failure : Private {
+ my ( $self, $c ) = @_;
+
+ $c->stash->{oauth_failure} = 1;
+ if ($c->session->{oauth}{detach_to}) {
+ $c->detach($c->session->{oauth}{detach_to}, $c->session->{oauth}{detach_args});
+ } else {
+ $c->stash->{template} = 'auth/general.html';
+ $c->detach;
+ }
+}
+
+sub oauth_success : Private {
+ my ($self, $c, $type, $uid, $name, $email) = @_;
+
+ my $user;
+ if ($email) {
+ # Only Facebook gets here
+ # We've got an ID and an email address
+ # Remove any existing mention of this ID
+ my $existing = $c->model('DB::User')->find( { facebook_id => $uid } );
+ $existing->update( { facebook_id => undef } ) if $existing;
+ # Get or create a user, give it this Facebook ID
+ $user = $c->model('DB::User')->find_or_new( { email => $email } );
+ $user->facebook_id($uid);
+ $user->name($name);
+ $user->in_storage() ? $user->update : $user->insert;
+ } else {
+ # We've got an ID, but no email
+ $user = $c->model('DB::User')->find( { $type . '_id' => $uid } );
+ if ($user) {
+ # Matching ID in our database
+ $user->name($name);
+ $user->update;
+ } else {
+ # No matching ID, store ID for use later
+ $c->session->{oauth}{$type . '_id'} = $uid;
+ $c->stash->{oauth_need_email} = 1;
+ }
+ }
+
+ # If we've got here with a full user, log in
+ if ($user) {
+ $c->authenticate( { email => $user->email, email_verified => 1 }, 'no_password' );
+ $c->stash->{login_success} = 1;
+ }
+
+ if ($c->session->{oauth}{detach_to}) {
+ $c->detach($c->session->{oauth}{detach_to}, $c->session->{oauth}{detach_args});
+ } elsif ($c->stash->{oauth_need_email}) {
+ $c->stash->{template} = 'auth/general.html';
+ } else {
+ $c->detach( '/auth/redirect_on_signin', [ $c->session->{oauth}{return_url} ] );
+ }
+}
+
+__PACKAGE__->meta->make_immutable;
+
+1;