Ado

 view release on metacpan or  search on metacpan

lib/Ado/Plugin/Auth.pm  view on Meta::CPAN

        $c->stash(error_login => 'Please choose one of the supported login methods.');
        $c->render(status => 401, template => 'login');
        return;
    }
    return;
}

#used as helper 'login_ado' returns 1 on success, '' otherwise
sub _login_ado {
    my ($c) = @_;

    #1. do basic validation first
    my $val = $c->validation;
    return '' unless $val->has_data;
    if ($val->csrf_protect->has_error('csrf_token')) {
        delete $c->session->{csrf_token};
        $c->render(error_login => 'Bad CSRF token!', status => 403, template => 'login');
        return '';
    }
    my $_checks = Ado::Model::Users->CHECKS;
    $val->required('login_name')->like($_checks->{login_name}{allow});
    $val->required('digest')->like(qr/^[0-9a-f]{40}$/);
    if ($val->has_error) {
        delete $c->session->{csrf_token};
        return '';
    }

    #2. find the user and do logical checks
    my $login_name = $val->param('login_name');
    my $user       = Ado::Model::Users->by_login_name($login_name);
    if ((not $user->id) or $user->disabled) {
        delete $c->session->{csrf_token};
        $c->stash(error_login_name => "No such user '$login_name'!");
        return '';
    }

    #3. really authnticate the user
    my $checksum = Mojo::Util::sha1_hex($c->session->{csrf_token} . $user->login_password);
    if ($checksum eq $val->param('digest')) {
        $c->session(login_name => $user->login_name);
        $c->user($user);
        $c->app->log->info('$user ' . $user->login_name . ' logged in!');
        delete $c->session->{csrf_token};
        return 1;
    }

    $c->debug('We should not be here! - wrong password') if $Ado::Control::DEV_MODE;
    delete $c->session->{csrf_token};
    return '';
}

#used as helper within login()
# this method is called as return_url after the user
# agrees or denies access for the application
sub _login_google {
    my ($c) = @_;
    state $app = $c->app;
    my $provider  = $c->param('auth_method');
    my $providers = $app->config('Ado::Plugin::Auth')->{providers};

    #second call should get the token it self
    my $response = $c->oauth2->get_token($provider, $providers->{$provider});
    do {
        $c->debug("in _login_google \$response: " . $c->dumper($response));
        $c->debug("in _login_google error from provider: " . ($c->param('error') || 'no error'));
    } if $Ado::Control::DEV_MODE;
    if ($response->{access_token}) {    #Athenticate, create and login the user.
        return _create_or_authenticate_google_user(
            $c,
            $response->{access_token},
            $providers->{$provider}
        );
    }
    else {
        #Redirect to front-page and say sorry
        # We are very sorry but we need to know you are a reasonable human being.
        $c->flash(error_login => $c->l('oauth2_sorry[_1]', ucfirst($provider))
              . ($c->param('error') || ''));
        $c->app->log->error('error_response:' . $c->dumper($response));
        $c->res->code(307);    #307 Temporary Redirect
        $c->redirect_to('/');
    }
    return;
}

#used as helper within login()
# this method is called as return_url after the user
# agrees or denies access for the application
sub _login_facebook {
    my ($c) = @_;
    state $app = $c->app;
    my $provider  = $c->param('auth_method');
    my $providers = $app->config('Ado::Plugin::Auth')->{providers};

    #second call should get the token it self
    my $response = $c->oauth2->get_token($provider, $providers->{$provider});
    do {
        $c->debug("in _login_facebook \$response: " . $c->dumper($response));
        $c->debug(
            "in _login_facebook error from provider: " . ($c->param('error') || 'no error'));
    } if $Ado::Control::DEV_MODE;
    if ($response->{access_token}) {    #Athenticate, create and login the user.
        return _create_or_authenticate_facebook_user(
            $c,
            $response->{access_token},
            $providers->{$provider}
        );
    }
    else {
        #Redirect to front-page and say sorry
        # We are very sorry but we need to know you are a reasonable human being.
        $c->flash(error_login => $c->l('oauth2_sorry[_1]', ucfirst($provider))
              . ($c->param('error') || ''));
        $c->app->log->error('error_response:' . $c->dumper($response));
        $c->res->code(307);    #307 Temporary Redirect
        $c->redirect_to('/');
    }
    return;

}

sub _authenticate_oauth2_user {
    my ($c, $user, $time) = @_;
    if (   $user->disabled
        || ($user->stop_date != 0 && $user->stop_date < $time)
        || $user->start_date > $time)
    {
        $c->flash(login_message => $c->l('oauth2_disabled'));
        $c->redirect_to('/');
        return;
    }
    $c->session(login_name => $user->login_name);
    $c->user($user);
    $c->app->log->info('$user ' . $user->login_name . ' logged in!');
    return 1;
}

#Creates a user using given info from provider
sub _create_oauth2_user {
    my ($c, $user_info, $provider) = @_;
    state $app = $c->app;
    if (my $user = Ado::Model::Users->add(_user_info_to_args($user_info, $provider))) {
        $app->plugins->emit_hook(after_user_add => $c, $user, $user_info);
        $c->user($user);
        $c->session(login_name => $user->login_name);
        $app->log->info($user->description . ' New $user ' . $user->login_name . ' logged in!');
        $c->flash(login_message => $c->l('oauth2_wellcome[_1]', $user->name));
        $c->redirect_to('/');
        return 1;
    }
    $app->log->error($@);
    return;
}

#next two methods
#(_create_or_authenticate_facebook_user and _create_or_authenticate_google_user)
# exist only because we pass different parameters in the form
# which are specific to the provider.
# TODO: think of a way to map the generation of the form arguments to the
# specific provider so we can dramatically reduce the number of provider
# specific subroutines
sub _create_or_authenticate_facebook_user {
    my ($c, $access_token, $provider) = @_;
    my $ua = Mojo::UserAgent->new;
    my $appsecret_proof = Digest::SHA::hmac_sha256_hex($access_token, $provider->{secret});
    $c->debug('$appsecret_proof:' . $appsecret_proof);
    my $user_info =
      $ua->get($provider->{info_url},
        form => {access_token => $access_token, appsecret_proof => $appsecret_proof})->res->json;
    $c->debug('Response from info_url:' . $c->dumper($user_info)) if $Ado::Control::DEV_MODE;

    my $user = Ado::Model::Users->by_email($user_info->{email});
    my $time = time;

    if ($user->id) {
        return _authenticate_oauth2_user($c, $user, $time);
    }

    #else create the user
    return _create_oauth2_user($c, $user_info, $provider);
}

sub _create_or_authenticate_google_user {
    my ($c, $access_token, $provider) = @_;

    #make request for the user info
    my $token_type = 'Bearer';
    my $ua         = Mojo::UserAgent->new;
    my $user_info =
      $ua->get($provider->{info_url} => {Authorization => "$token_type $access_token"})
      ->res->json;

    my $user = Ado::Model::Users->by_email($user_info->{email});
    my $time = time;

    if ($user->id) {
        return _authenticate_oauth2_user($c, $user, $time);
    }

    #else create the user
    return _create_oauth2_user($c, $user_info, $provider);
}

# Redirects to Consent screen
sub authorize {
    my ($c)    = @_;
    my $m      = $c->param('auth_method');
    my $params = $c->app->config('Ado::Plugin::Auth')->{providers}{$m};
    $params->{redirect_uri} = '' . $c->url_for("/login/$m")->to_abs;

    #This call will redirect the user to the provider Consent screen.
    $c->redirect_to($c->oauth2->auth_url($m, %$params));
    return;
}

# Maps user info given from provider to arguments for
# Ado::Model::Users->new
sub _user_info_to_args {
    my ($ui, $provider) = @_;
    my %args;
    if (index($provider->{info_url}, 'google') > -1) {
        $args{first_name} = $ui->{given_name};
        $args{last_name}  = $ui->{family_name};
    }
    elsif (index($provider->{info_url}, 'facebook') > -1) {
        $args{first_name} = $ui->{first_name};
        $args{last_name}  = $ui->{last_name};
    }

    #Add another elsif to map different %args to $ui from a new provider
    else {
        Carp::croak('Unknown provider info_url:' . $provider->{info_url});
    }
    $args{email}      = $ui->{email};
    $args{login_name} = $ui->{email};
    $args{login_name} =~ s/[\@\.]+//g;
    $args{login_password} =
      Mojo::Util::sha1_hex($args{login_name} . Ado::Sessions->generate_id());
    $args{description} = "Registered via $provider->{info_url}!";
    $args{created_by}  = $args{changed_by} = 1;
    $args{start_date}  = $args{disabled} = $args{stop_date} = 0;

    return %args;
}
1;


=pod

=encoding utf8

=head1 NAME

Ado::Plugin::Auth - Passwordless user authentication for Ado

=head1 SYNOPSIS

  #in etc/ado.$mode.conf
  plugins =>[
    #...
    'auth',
    #...
  ],

    #in etc/plugins/auth.$mode.conf
    {
      #methods which will be displayed in the "Sign in" menu
      auth_methods => ['ado', 'facebook', 'google'],

      providers => {
        google => {
            key =>'123456789....apps.googleusercontent.com',
            secret =>'YourSECR3T',
            scope=>'profile email',
            info_url => 'https://www.googleapis.com/userinfo/v2/me',
        },
        facebook => {
            key =>'123456789',
            secret =>'123456789abcdef',
            scope =>'public_profile,email',
            info_url => 'https://graph.facebook.com/v2.2/me',
        },
      }
    }

=head1 DESCRIPTION

L<Ado::Plugin::Auth> is a plugin that authenticates users to an L<Ado> system.
Users can be authenticated via Google, Facebook, locally and in the future
other authentication service-providers.

B<Note that the user's pasword is never sent over the network>. When using the
local authentication method (ado) a digest is prepared in the browser using
JavaScript. The digest is sent and compared on the server side. The digest is
different in every POST request. The other authentication methods use the
services provided by well known service providers like Google, Facebook etc.
To use external authentication providers the module
L<Mojolicious::Plugin::OAuth2> needs to be installed.

=head1 CONFIGURATION

The following options can be set in C<etc/plugins/auth.$mode.conf>. You can
find default options in C<etc/plugins/auth.conf>.

=head2 auth_methods

This option will enable the listed methods (services) which will be used to
authenticate a user. The services will be listed in the specified order in the
partial template C<authbar.html.ep> that can be included in any other template
on your site.

  #in etc/plugins/auth.$mode.conf
  {
    #methods which will be displayed in the "Sign in" menu
    auth_methods => ['ado', 'google'],
  }

=head2 providers

A Hash reference with keys representing names of providers (same as
auth_methods) and values, containing the configurations for the specific
providers. This option will be merged with already defined providers by
L<Mojolicious::Plugin::OAuth2>. Add the rest of the needed configuration
options to auth.development.conf or auth.production.conf only because this is
highly sensitive and application specific information.

  #Example for google:
  google =>{
      #client_id
      key =>'123456654321abcd.apps.googleusercontent.com',
      secret =>'Y0uRS3cretHEre',
      scope=>'profile email',
      info_url => 'https://www.googleapis.com/userinfo/v2/me',
      },

=head2 routes

Currently defined routes are described in L</ROUTES>.

=head1 CONDITIONS

L<Ado::Plugin::Auth> provides the following conditions to be used by routes.
To find more about conditions read L<Mojolicious::Guides::Routing/Conditions>.

=head2 authenticated

Condition for routes used to check if a user is authenticated.

=cut

#TODO:?
#Additional parameters can be passed to specify the preferred
#authentication method to be preselected in the login form
#if condition redirects to C</login/:auth_method>.

=pod

  # add the condition programatically
  $app->routes->route('/ado-users/:action', over => {authenticated=>1});
  $app->routes->route('/ado-users/:action',
    over => [authenticated => 1, ingroup => 'admin']
  );

  #in etc/ado.$mode.conf or etc/plugins/foo.$mode.conf
  routes => [
    #...
    {
      route => '/ado-users/:action:id',
      via   => [qw(PUT DELETE)],

      # only authenticated users can edit and delete users,
      # and only if they are authorized to do so
      over => [authenticated => 1, ingroup => 'admin'],
      to =>'ado-users#edit'
    }
  ],

=head2 ingroup

Checks if a user is in the given group. Returns true or false.

  # in etc/plugins/routes.conf or etc/plugins/foo.conf
  {
    route => '/vest',
    via => ['GET'],
    to => 'vest#screen',
    over => [authenticated => 1, ingroup => 'foo'],
  }
  # programatically
  $app->routes->route('/ado-users/:action', over => {ingroup => 'foo'});



( run in 1.091 second using v1.01-cache-2.11-cpan-39bf76dae61 )