I have successfully integrated Login through Google Oauth2 method into my app, where I am using IonAuth authentication library. I will share the steps, and hopefully, if someone notices anything insecure in my code, please let me know...
So, first of all, I needed to create an app on
https://console.cloud.google.com/apis/credentials with Oauth consent screen. That is easy to do following a video tutorial here:
https://www.youtube.com/watch?v=a5puLR051Is so I will not go into details.
Then I installed google api client php library with dependencies, easiest done via Composer:
Code:
composer require google/apiclient:^2.12
It is advisable to then remove all the unnecessary stuff 100+ Google services, to thin the library. You will only need Oauth2 service, everything else can go (see the library page on packagist how that is done).
Then I needed to extend the IonAuth library.
Created an extended model and placed it in app/Models:
PHP Code:
<?php
namespace App\Models;
class IonAuthModel extends \IonAuth\Models\IonAuthModel
{
// admittedly, this will only work if $identity is email
// it needs a little tweaking so that the query below searches not for identity (which could be email or username), but
// for email proper, as Google will return email; will fix that in the future
// function is a heavily modified version of the login() function
public function login_google(string $identity): bool
{
$this->triggerEvents('pre_login');
if (empty($identity)) {
$this->setError('IonAuth.login_unsuccessful');
return false;
}
$this->triggerEvents('extra_where');
$query = $this->db->table($this->tables['users'])
->select($this->identityColumn . ', email, id, password, active, last_login')
->where($this->identityColumn, $identity)
->limit(1)
->orderBy('id', 'desc')
->get();
$user = $query->getRow();
// if we foun the user by the email supplied by Google...
if (isset($user)) {
// and if the user is not inactive...
if ($user->active == 0) {
$this->triggerEvents('post_login_unsuccessful');
$this->setError('IonAuth.login_unsuccessful_not_active');
return false;
}
// we declare success!
$this->setSession($user);
$this->updateLastLogin($user->id);
// hardly needed, but why not
$this->clearLoginAttempts($identity);
$this->clearForgottenPasswordCode($identity);
$this->session->regenerate(false);
$this->triggerEvents(['post_login', 'post_login_successful']);
$this->setMessage('IonAuth.login_successful');
return true;
}
$this->triggerEvents('post_login_unsuccessful');
// could write a line here that is specific to this method
$this->setError('IonAuth.login_unsuccessful');
return false;
}
}
Created an extended library and placed it in app/Libraries (only needed to extend the constructor to use the extended model, so perhaps I could have sourced the original function and only overwritten the $this->ionAuthModel property?):
PHP Code:
<?php
namespace App\Libraries;
class IonAuth extends \IonAuth\Libraries\IonAuth
{
public function __construct()
{
// Check compat first
$this->checkCompatibility();
$this->config = config('IonAuth');
$this->email = \Config\Services::email();
helper('cookie');
$this->session = session();
$this->ionAuthModel = model('IonAuthModel');
$emailConfig = $this->config->emailConfig;
if ($this->config->useCiEmail && isset($emailConfig) && is_array($emailConfig))
{
$this->email->initialize($emailConfig);
}
$this->ionAuthModel->triggerEvents('library_constructor');
}
}
Then I extended the Auth.php controller and placed it in app/Controllers (to be fair, the controller has long been extended, so you will see some of my own code there):
PHP Code:
<?php
namespace App\Controllers;
use Google;
class Auth extends \IonAuth\Controllers\Auth
{
//I need an extended constructor:
public function __construct()
{
parent::__construct();
//this is my new extended IonAuth library
$this->ionAuth = new \App\Libraries\IonAuth();
// some of my stuff was here...
//google client code
$this->googleClient = new Google\Client();
$this->googleClient->setClientId("this-is-your-client-id.apps.googleusercontent.com");
$this->googleClient->setClientSecret("this-is-your-client-secret");
$this->googleClient->setRedirectUri(base_url('auth/login_google'));
// I will only need email, since that is all I want Google to ascertain
$this->googleClient->addScope("email");
}
public function login()
{
// skipping everything, only wanted to show you the
// google login button that should be
// passed to the views along the data:
// so lots of commented out code here
$this->data['google_button'] = '<a class="btn btn-info" href="'.$this->googleClient->createAuthUrl().'" >Prisijungti per <i class="bi bi-google"></i></a>';
echo view('common/header_nologin_view', $this->data);
echo view('auth/login_view', $this->data);
echo view('common/footer_nologin_view');
// } commented out condition bracket
}
// our most important method here, which gets data from google, passes it through the library
// to the model, where it gets verified and access is then granted
public function login_google()
{
$token = $this->googleClient->fetchAccessTokenWithAuthCode($this->request->getVar('code'));
if(!isset($token['error'])){
$this->googleClient->setAccessToken($token['access_token']);
session()->set("AccessToken", $token['access_token']);
$googleService = new \Google_Service_Oauth2($this->googleClient);
// here we get an email of the user that Google swears really belongs
// to the person trying to login
$user_email = $googleService->userinfo->get()->email;
}
else {
throw \CodeIgniter\Exceptions\PageNotFoundException::forPageNotFound($token['error']);
}
// the controller method calls the library which directly calls the model here
if ($this->ionAuth->login_google($user_email)) {
//if the login is successful
//redirect them back to the home page
$this->session->setFlashdata('message', $this->ionAuth->messages());
return redirect()->to('/')->withCookies();
} else {
// if the login was un-successful
// redirect them back to the login page
$this->session->setFlashdata('message', $this->ionAuth->errors($this->validationListTemplate));
return redirect()->back()->withInput();
}
}
// all the other controller methods here
}
Since my app functionality is not publically accesible, I also added the new route auth/login_google to the exceptions list of the filter that checks login in app/Config/Filters.php and to the list of filter that checks if someone is trying to access a route being logged in that is only available to non-logged in users. Here is the full file:
PHP Code:
<?php
namespace Config;
use CodeIgniter\Config\BaseConfig;
use CodeIgniter\Filters\CSRF;
use CodeIgniter\Filters\DebugToolbar;
use CodeIgniter\Filters\Honeypot;
class Filters extends BaseConfig
{
/**
* Configures aliases for Filter classes to
* make reading things nicer and simpler.
*
* @var array
*/
public $aliases = [
'csrf' => CSRF::class,
'toolbar' => DebugToolbar::class,
'honeypot' => Honeypot::class,
'checkLogin' => \App\Filters\CheckLogin::class,
'checkNonLogin' => \App\Filters\CheckNonLogin::class,
'checkMaintenanceMode' => \App\Filters\CheckMaintenanceMode::class,
];
/**
* List of filter aliases that are always
* applied before and after every request.
*
* @var array
*/
public $globals = [
'before' => [
'checkLogin' => [
'except' => [
//list all exceptions
'cron/*', //cron needs access
'auth/login',
'auth/login_google',
'auth/forgot_password',
'auth/reset_password/*'
]
] //just add like this
// 'honeypot',
// 'csrf',
],
'after' => [
'toolbar',
// 'honeypot',
],
];
/**
* List of filter aliases that works on a
* particular HTTP method (GET, POST, etc.).
*
* Example:
* 'post' => ['csrf', 'throttle']
*
* @var array
*/
public $methods = [];
/**
* List of filter aliases that should run on any
* before or after URI patterns.
*
* Example:
* 'isLoggedIn' => ['before' => ['account/*', 'profiles/*']]
*
* @var array
*/
public $filters = [
// should be moved to globals?
'checkMaintenanceMode' => [
'before' =>
[
'',
'*',
]
],
// should be moved to globals?
'checkNonLogin' => [
'before' =>
[
'auth/login',
'auth/login_google',
'auth/forgot_password',
'auth/reset_password/*',
]
],
];
}
Finally I added the Google button line to the login view (hope it is clear enough, only posting that one line):
I think that was all. Now, when I open the login page at
https://mysecretwebsite.lt/auth/login , besides the main login form I see a Google login button. When I press it I am redirected to Google Oauth screen where I authenticate and get redirected back into my app.
It was so easy that I am curious if it is all that secure... So, does my code look secure?
Hope it will be useful to someone.
==
Donatas G.