diff options
Diffstat (limited to 'perllib/FixMyStreet/App/Controller')
-rw-r--r-- | perllib/FixMyStreet/App/Controller/Auth.pm | 30 | ||||
-rw-r--r-- | perllib/FixMyStreet/App/Controller/Auth/Social.pm | 129 | ||||
-rw-r--r-- | perllib/FixMyStreet/App/Controller/Report/New.pm | 15 | ||||
-rw-r--r-- | perllib/FixMyStreet/App/Controller/Report/Update.pm | 15 |
4 files changed, 151 insertions, 38 deletions
diff --git a/perllib/FixMyStreet/App/Controller/Auth.pm b/perllib/FixMyStreet/App/Controller/Auth.pm index 50d11a10c..b2c001566 100644 --- a/perllib/FixMyStreet/App/Controller/Auth.pm +++ b/perllib/FixMyStreet/App/Controller/Auth.pm @@ -44,13 +44,12 @@ sub general : Path : Args(0) { # decide which action to take $c->detach('code_sign_in') if $clicked_sign_in_by_code || ($data_email && !$data_password); - if (!$data_username && !$data_password && !$data_email) { - $c->detach('social/facebook_sign_in') if $c->get_param('facebook_sign_in'); - $c->detach('social/twitter_sign_in') if $c->get_param('twitter_sign_in'); + if (!$data_username && !$data_password && !$data_email && $c->get_param('social_sign_in')) { + $c->forward('social/handle_sign_in'); } - $c->forward( 'sign_in', [ $data_username ] ) - && $c->detach( 'redirect_on_signin', [ $c->get_param('r') ] ); + $c->forward( 'sign_in', [ $data_username ] ) + && $c->detach( 'redirect_on_signin', [ $c->get_param('r') ] ); } @@ -180,13 +179,13 @@ sub email_sign_in : Private { name => $c->get_param('name'), password => $user->password, }; - $token_data->{name} = $c->session->{oauth}{name} - if $c->get_param('oauth_need_email') && $c->session->{oauth}{name} && !$token_data->{name}; - $token_data->{facebook_id} = $c->session->{oauth}{facebook_id} - if $c->get_param('oauth_need_email') && $c->session->{oauth}{facebook_id}; - $token_data->{twitter_id} = $c->session->{oauth}{twitter_id} - if $c->get_param('oauth_need_email') && $c->session->{oauth}{twitter_id}; + if ($c->get_param('oauth_need_email')) { + $token_data->{name} = $c->session->{oauth}{name} + if $c->session->{oauth}{name} && !$token_data->{name}; + $c->forward('set_oauth_token_data', [ $token_data ]); + } + if ($c->stash->{current_user}) { $token_data->{old_user_id} = $c->stash->{current_user}->id; $token_data->{r} = 'auth/change_email/success'; @@ -217,6 +216,14 @@ sub get_token : Private { return $data; } +sub set_oauth_token_data : Private { + my ( $self, $c, $token_data ) = @_; + + foreach (qw/facebook_id twitter_id oidc_id/) { + $token_data->{$_} = $c->session->{oauth}{$_} if $c->session->{oauth}{$_}; + } +} + =head2 token Handle the 'email_sign_in' tokens. Find the account for the email address @@ -275,6 +282,7 @@ sub process_login : Private { $user->password( $data->{password}, 1 ) if $data->{password}; $user->facebook_id( $data->{facebook_id} ) if $data->{facebook_id}; $user->twitter_id( $data->{twitter_id} ) if $data->{twitter_id}; + $user->add_oidc_id( $data->{oidc_id} ) if $data->{oidc_id}; $user->update_or_insert; $c->authenticate( { $type => $data->{$type}, $ver => 1 }, 'no_password' ); diff --git a/perllib/FixMyStreet/App/Controller/Auth/Social.pm b/perllib/FixMyStreet/App/Controller/Auth/Social.pm index d1a2c37cb..e0abf5c60 100644 --- a/perllib/FixMyStreet/App/Controller/Auth/Social.pm +++ b/perllib/FixMyStreet/App/Controller/Auth/Social.pm @@ -6,6 +6,7 @@ BEGIN { extends 'Catalyst::Controller'; } use Net::Facebook::Oauth2; use Net::Twitter::Lite::WithAPIv1_1; +use OIDC::Lite::Client::WebServer::Azure; =head1 NAME @@ -13,10 +14,26 @@ FixMyStreet::App::Controller::Auth::Social - Catalyst Controller =head1 DESCRIPTION -Controller for the Facebook/Twitter authentication. +Controller for the Facebook/Twitter/OpenID Connect authentication. =head1 METHODS +=head2 handle_sign_in + +Forwards to the appropriate (facebook|twitter|oidc)_sign_in method +based on the social_sign_in parameter + +=cut + +sub handle_sign_in : Private { + my ($self, $c) = @_; + + $c->detach('facebook_sign_in') if $c->get_param('social_sign_in') eq 'facebook'; + $c->detach('twitter_sign_in') if $c->get_param('social_sign_in') eq 'twitter'; + $c->detach('oidc_sign_in') if $c->get_param('social_sign_in') eq 'oidc'; + +} + =head2 facebook_sign_in Starts the Facebook authentication sequence. @@ -142,6 +159,84 @@ sub twitter_callback: Path('/auth/Twitter') : Args(0) { $c->forward('oauth_success', [ 'twitter', $info->{id}, $info->{name} ]); } +sub oidc : Private { + my ($self, $c) = @_; + + my $config = $c->cobrand->feature('oidc_login'); + + OIDC::Lite::Client::WebServer::Azure->new( + id => $config->{client_id}, + secret => $config->{secret}, + authorize_uri => $config->{auth_uri}, + access_token_uri => $config->{token_uri}, + ); +} + +sub oidc_sign_in : Private { + my ( $self, $c ) = @_; + + $c->detach( '/page_error_403_access_denied', [] ) if FixMyStreet->config('SIGNUPS_DISABLED'); + $c->detach( '/page_error_400_bad_request', [] ) unless $c->cobrand->feature('oidc_login'); + + my $oidc = $c->forward('oidc'); + my $url = $oidc->uri_to_redirect( + redirect_uri => $c->uri_for('/auth/OIDC'), + scope => 'openid', + state => 'test', + extra => { + response_mode => 'form_post', + }, + ); + + 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); +} + +sub oidc_callback: Path('/auth/OIDC') : Args(0) { + my ( $self, $c ) = @_; + + $c->detach('oauth_failure') if $c->get_param('error'); + $c->detach('/page_error_400_bad_request', []) unless $c->get_param('code'); + + my $oidc = $c->forward('oidc'); + + my $id_token; + eval { + $id_token = $oidc->get_access_token( + code => $c->get_param('code'), + ); + }; + if ($@) { + (my $message = $@) =~ s/at [^ ]*Auth.pm.*//; + $c->detach('/page_error_500_internal_error', [ $message ]); + } + + $c->detach('oauth_failure') unless $id_token; + + # sanity check the token audience is us... + $c->detach('/page_error_500_internal_error', ['invalid id_token']) unless $id_token->payload->{aud} eq $c->cobrand->feature('oidc_login')->{client_id}; + + # Some claims need parsing into a friendlier format + # XXX check how much of this is Westminster/Azure-specific + my $name = join(" ", $id_token->payload->{given_name}, $id_token->payload->{family_name}); + my $email = $id_token->payload->{email}; + # WCC Azure provides a single email address as an array for some reason + my $emails = $id_token->payload->{emails}; + if ($emails && @$emails) { + $email = $emails->[0]; + } + + # There's a chance that a user may have multiple OIDC logins, so build a namespaced uid to prevent collisions + my $uid = join(":", $c->cobrand->moniker, $c->cobrand->feature('oidc_login')->{client_id}, $id_token->payload->{sub}); + + $c->forward('oauth_success', [ 'oidc', $uid, $name, $email ]); +} + + sub oauth_failure : Private { my ( $self, $c ) = @_; @@ -159,19 +254,39 @@ sub oauth_success : Private { my $user; if ($email) { - # Only Facebook gets here + # Only Facebook & OIDC get 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 + my $existing; + if ($type eq 'facebook') { + $existing = $c->model('DB::User')->find( { $type . '_id' => $uid } ); + $existing->update( { $type . '_id' => undef } ) if $existing; + } elsif ( $type eq 'oidc' ) { + $existing = $c->model('DB::User')->find( { oidc_ids => \[ + '&& ?', [ oidc_ids => [ $uid ] ] + ] } ); + $existing->remove_oidc_id( $uid ) if $existing; + } + + # Get or create a user, give it this Facebook/OIDC ID $user = $c->model('DB::User')->find_or_new( { email => $email } ); - $user->facebook_id($uid); + if ( $type eq 'facebook' ) { + $user->facebook_id($uid); + } elsif ( $type eq 'oidc' ) { + $user->add_oidc_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 ($type eq 'oidc') { + $user = $c->model('DB::User')->find( { oidc_ids => \[ + '&& ?', [ oidc_ids => [ $uid ] ] + ] } ); + } else { + $user = $c->model('DB::User')->find( { $type . '_id' => $uid } ); + } if ($user) { # Matching ID in our database $user->name($name); diff --git a/perllib/FixMyStreet/App/Controller/Report/New.pm b/perllib/FixMyStreet/App/Controller/Report/New.pm index 120467905..7a3e94f95 100644 --- a/perllib/FixMyStreet/App/Controller/Report/New.pm +++ b/perllib/FixMyStreet/App/Controller/Report/New.pm @@ -1155,7 +1155,7 @@ sub check_for_errors : Private { } # if using social login then we don't care about other errors - $c->stash->{is_social_user} = $c->get_param('facebook_sign_in') || $c->get_param('twitter_sign_in'); + $c->stash->{is_social_user} = $c->get_param('social_sign_in') ? 1 : 0; if ( $c->stash->{is_social_user} ) { delete $field_errors{name}; delete $field_errors{username}; @@ -1197,10 +1197,8 @@ sub tokenize_user : Private { password => $report->user->password, title => $report->user->title, }; - $c->stash->{token_data}{facebook_id} = $c->session->{oauth}{facebook_id} - if $c->get_param('oauth_need_email') && $c->session->{oauth}{facebook_id}; - $c->stash->{token_data}{twitter_id} = $c->session->{oauth}{twitter_id} - if $c->get_param('oauth_need_email') && $c->session->{oauth}{twitter_id}; + $c->forward('/auth/set_oauth_token_data', [ $c->stash->{token_data} ]) + if $c->get_param('oauth_need_email'); } sub send_problem_confirm_email : Private { @@ -1308,6 +1306,7 @@ sub process_confirmation : Private { for (qw(name title facebook_id twitter_id)) { $problem->user->$_( $data->{$_} ) if $data->{$_}; } + $problem->user->add_oidc_id($data->{oidc_id}) if $data->{oidc_id}; $problem->user->update; } if ($problem->user->email_verified) { @@ -1368,11 +1367,7 @@ sub save_user_and_report : Private { $c->stash->{detach_to} = '/report/new/oauth_callback'; $c->stash->{detach_args} = [$token->token]; - if ( $c->get_param('facebook_sign_in') ) { - $c->detach('/auth/social/facebook_sign_in'); - } elsif ( $c->get_param('twitter_sign_in') ) { - $c->detach('/auth/social/twitter_sign_in'); - } + $c->forward('/auth/social/handle_sign_in') if $c->get_param('social_sign_in'); } # Save or update the user if appropriate diff --git a/perllib/FixMyStreet/App/Controller/Report/Update.pm b/perllib/FixMyStreet/App/Controller/Report/Update.pm index cbedf7a01..68c8a41ce 100644 --- a/perllib/FixMyStreet/App/Controller/Report/Update.pm +++ b/perllib/FixMyStreet/App/Controller/Report/Update.pm @@ -366,7 +366,7 @@ sub check_for_errors : Private { ); # if using social login then we don't care about name and email errors - $c->stash->{is_social_user} = $c->get_param('facebook_sign_in') || $c->get_param('twitter_sign_in'); + $c->stash->{is_social_user} = $c->get_param('social_sign_in') ? 1 : 0; if ( $c->stash->{is_social_user} ) { delete $field_errors{name}; delete $field_errors{username}; @@ -404,10 +404,8 @@ sub tokenize_user : Private { name => $update->user->name, password => $update->user->password, }; - $c->stash->{token_data}{facebook_id} = $c->session->{oauth}{facebook_id} - if $c->get_param('oauth_need_email') && $c->session->{oauth}{facebook_id}; - $c->stash->{token_data}{twitter_id} = $c->session->{oauth}{twitter_id} - if $c->get_param('oauth_need_email') && $c->session->{oauth}{twitter_id}; + $c->forward('/auth/set_oauth_token_data', [ $c->stash->{token_data} ]) + if $c->get_param('oauth_need_email'); } =head2 save_update @@ -440,11 +438,7 @@ sub save_update : Private { $c->stash->{detach_to} = '/report/update/oauth_callback'; $c->stash->{detach_args} = [$token->token]; - if ( $c->get_param('facebook_sign_in') ) { - $c->detach('/auth/social/facebook_sign_in'); - } elsif ( $c->get_param('twitter_sign_in') ) { - $c->detach('/auth/social/twitter_sign_in'); - } + $c->forward('/auth/social/handle_sign_in') if $c->get_param('social_sign_in'); } if ( $c->cobrand->never_confirm_updates ) { @@ -585,6 +579,7 @@ sub process_confirmation : Private { for (qw(name facebook_id twitter_id)) { $comment->user->$_( $data->{$_} ) if $data->{$_}; } + $comment->user->add_oidc_id($data->{oidc_id}) if $data->{oidc_id}; $comment->user->password( $data->{password}, 1 ) if $data->{password}; $comment->user->update; } |