This tutorial is part of the Building Your Startup With PHP series on Envato Tuts+. In this series, I'm guiding you through launching a startup from concept to reality using my Meeting Planner app as a real-life example. Every step along the way, I'll release the Meeting Planner code as open-source examples you can learn from. I'll also address startup-related business issues as they arise.
In this tutorial, I'll guide you through implementing OAuth integration with common social networks to make signing up and repeated usage easier and more efficient. I'll explore Facebook, Google, Twitter and LinkedIn, the networks I see as being most appropriate to Meeting Planner's target users.
All of the code for Meeting Planner is written in the Yii2 Framework for PHP. If you'd like to learn more about Yii2, check out our parallel series Programming With Yii2 at Envato Tuts+.
If you haven't tried out Meeting Planner yet, try scheduling your first meeting now. It's really beginning to come together this year. Ultimately, I was able to use Yii2's built-in AuthClient support to provide sign-in from all of the networks above—so you can use these to register now.
Feedback is welcome. If you have a question or topic suggestion, please post a comment below. You can also reach me on Twitter @reifman.
What Is AuthClient?
AuthClient is Yii's built-in support for your applications to authenticate via third-party services with OpenID, OAuth, or OAuth2.
If you followed my Yii2 series in June 2015, you'd have seen me using AuthClient to integrate with Google via OpenID, but the company ended its support for the specification shortly afterwards. Then, in December, I wrote a tutorial which used the Yii2-User extension to add Google OAuth support—the Yii2 Framework didn't yet have this. However, Yii2-User doesn't integrate well with established codebases that already have a user-oriented codebase. But luckily, the Yii2 Framework had since added support for Google OAuth, and everything has become more straightforward.
In this tutorial, I'll guide you through using the new AuthClient functionality to integrate with a variety of popular social networks. Out of the box, Yii provides support for the following clients:
Another motivation for supporting connection to Meeting Planner via social networks is that it allows people to show up and easily share their name and email with us. With email and password registration, we don't actually ever learn their name. However, Twitter, unlike other social networks, creates significantly problematic barriers to obtaining users' email addresses, which ultimately led me to disable it for now.
Let's get started with the code integration.
Installing AuthClient in Our Application
First, we need to install the Yii components for OAuth, Yii's AuthClient.
Add AuthClient to Composer
Let's add the AuthClient library to composer.json:
"minimum-stability": "stable", "require": { "php": ">=5.4.0", "yiisoft/yii2": "*", "yiisoft/yii2-bootstrap": "*", "yiisoft/yii2-swiftmailer": "*", "2amigos/yii2-google-maps-library": "*", "2amigos/yii2-google-places-library": "*", "stichoza/google-translate-php": "~2.0", "2amigos/yii2-date-time-picker-widget": "*", "yiisoft/yii2-jui": "*", "cebe/yii2-gravatar": "*", "kartik-v/yii2-widget-fileinput": "*", "kartik-v/yii2-widget-switchinput": "*", "yiisoft/yii2-imagine": "*", "2amigos/yii2-resource-manager-component": "0.1.*", "yiisoft/yii2-authclient": "~2.0.0" },
Then, we need to update composer:
sudo composer update Password: Loading composer repositories with package information Updating dependencies (including require-dev) - Updating 2amigos/yii2-date-time-picker-widget (0.1.0 => 0.1.1) Checking out 572e2448ba1cd207b339dd5d117e3d1d23f0bbc3 - Installing yiisoft/yii2-authclient (2.0.2) Loading from cache Writing lock file Generating autoload files
Configuring AuthClient Support
And, we need to add the AuthClient configuration settings to our configuration file in \frontend\config\main.php
.
Add array elements for all of the third-party services that you wish to support (details for each can be found in the AuthClient Guide):
'components' => [ 'authClientCollection' => [ 'class' => 'yii\authclient\Collection', 'clients' => [ 'facebook' => [ 'class' => 'yii\authclient\clients\Facebook', 'clientId' => $config['oauth_fb_id'], 'clientSecret' => $config['oauth_fb_secret'], ], 'google' => [ 'class' => 'yii\authclient\clients\GoogleOAuth', 'clientId' => $config['oauth_google_client_id'], 'clientSecret' => $config['oauth_google_client_secret'], ], 'linkedin' => [ 'class' => 'yii\authclient\clients\LinkedIn', 'clientId' => $config['linkedin_client_id'], 'clientSecret' => $config['linkedin_client_secret'], ], 'twitter' => [ 'class' => 'yii\authclient\clients\Twitter', 'consumerKey' => $config['oauth_twitter_key'], 'consumerSecret' => $config['oauth_twitter_secret'], ], ], ],
In order to obtain codes for all those keys and secrets, you need to register your application with each social network. This can often be time consuming.
Registering Developer Applications
Follow along as I walk you through signing up with some of the networks and some of the deeper configuration aspects with others.
Registering With Twitter
Create a new Twitter application at the Twitter Application Dashboard:
Click Create New App—I found that the callback URL was unnecessary, but for now I used the placeholder http://mydomain.com/user/security/auth.
Here's the new page for our application:
Here's the Settings page:
Here's the Keys and Access Tokens page. Here, we need to copy the Consumer Key (API Key) and Consumer Secret (API Secret):
Those keys go in our mp.ini file, which is read into the $config
variable above to configure AuthClient for Twitter.
Register our Facebook Application
Next, let's visit the Facebook developer console and Add a New App:
We'll choose to create a WWW Website app for now:
Provide the name of our application:
And collect our new App ID:
They ask for all the regular information, such as URLs:
And then you can find our Meeting Planner app in the list:
Here's the Facebook dashboard for your application:
Register With Google
Google APIs are a bit more complex than Twitter and Facebook, so the UX is a little harder to follow. But basically, once you create an application, you need OAuth 2.0 keys, which you get by opening the application area on the credentials screen:
That takes you here:
For security reasons, Google (and LinkedIn) require a full list of exactly which URL paths and parameters might be used during an OAuth sequence. While in development, this can require a lot of adjusting—even for testing from localhost.
Once you've entered them, you'll see them listed below:
Google does a nice job of helping you configure the consent screen which your users will see when they try to sign up or link their Google account to Meeting Planner:
Register With LinkedIn
LinkedIn is fairly simple compared to Google. You require the basic details for your application:
Like Google, they require all the URLs you'll be using in development and production. You can also obtain the keys on this page:
Placing Keys in Our Configuration File
In Protecting Your Keys From GitHub, I described in detail how I use a configuration file to store all of my keys apart from my GitHub repository. Then, I include this file at the beginning of my Yii configuration files. This keeps me from accidentally checking in my keys to my repository and compromising my accounts.
We place both the Twitter and Facebook Application keys and secrets into /var/secure/mp.ini
outside the repository:
oauth_fb_id="154xxxxxxxxxxxxxx33" oauth_fb_secret="bcxxxxxxxxxxxxxxdda" oauth_twitter_key ="JCpxxxxxxxxxxxxxxnsF" oauth_twitter_secret="f3xxxxxxxxxxxxxxxxxxxxxxxxxxxxu37" oauth_twitter_token="153xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxfBj" oauth_twitter_token_secret="Synxxxxxxxxxxxxxxxxxxxxxxxxxxxx4X" oauth_google_client_id = "1xxxxxxxxxxxxxxxxxxxxxxq.apps.googleusercontent.com" oauth_google_client_secret = "cfkxxxxxxxxxxxxxxox" linkedin_client_id = "7xxxxxxxxxxxxxxq" linkedin_client_secret ="IxxxxxxxxxxxxxxI"
Here again is the code in \frontend\config\main.php
which includes these settings and sets the individual configuration variables:
<?php $config = parse_ini_file('/var/secure/mp.ini', true); $params = array_merge( require(__DIR__ . '/../../common/config/params.php'), require(__DIR__ . '/../../common/config/params-local.php'), require(__DIR__ . '/params.php'), require(__DIR__ . '/params-local.php') ); return [ 'id' => 'app-frontend', 'basePath' => dirname(__DIR__), 'bootstrap' => ['log'], 'controllerNamespace' => 'frontend\controllers', 'components' => [ 'authClientCollection' => [ 'class' => 'yii\authclient\Collection', 'clients' => [ 'google' => [ 'class' => 'yii\authclient\clients\GoogleOpenId' ], 'facebook' => [ 'class' => 'yii\authclient\clients\Facebook', 'clientId' => $config['oauth_fb_id'], 'clientSecret' => $config['oauth_fb_secret'], ], 'twitter' => [ 'class' => 'yii\authclient\clients\Twitter', 'consumerKey' => $config['oauth_twitter_key'], 'consumerSecret' => $config['oauth_twitter_secret'], ], ], ], 'urlManager' => [
Updating the Schema to Store Session Keys
Now that we're ready to write code to integrate social signup and login, we need the database to create an Auth
table which will store the social service, its ID for the person, and the user_id
for that person in Meeting Planner:
./yii migrate/create create_auth_table Yii Migration Tool (based on Yii v2.0.2) Create new migration '/Users/Jeff/Sites/mp/console/migrations/m150227_235635_create_auth_table.php'? (yes|no) [no]:yes New migration created successfully.
Here's what the migration looks like:
<?php use yii\db\Schema; use yii\db\Migration; class m150227_235635_create_auth_table extends Migration { public function up() { $tableOptions = null; if ($this->db->driverName === 'mysql') { $tableOptions = 'CHARACTER SET utf8 COLLATE utf8_unicode_ci ENGINE=InnoDB'; } $this->createTable('{{%auth}}', [ 'id' => Schema::TYPE_PK, 'user_id' => Schema::TYPE_BIGINT.' NOT NULL', 'source' => Schema::TYPE_STRING.' NOT NULL', 'source_id' => Schema::TYPE_STRING.' NOT NULL', ], $tableOptions); $this->addForeignKey('fk-auth-user_id-user-id', '{{%auth}}', 'user_id', '{{%user}}', 'id', 'CASCADE', 'CASCADE'); } public function down() { $this->dropForeignKey('fk-auth-user_id-user-id', '{{%auth}}'); $this->dropTable('{{%auth}}'); } }
Here's the result when we run it:
./yii migrate/up Yii Migration Tool (based on Yii v2.0.2) Total 1 new migration to be applied: m150227_235635_create_auth_table Apply the above migration? (yes|no) [no]:yes *** applying m150227_235635_create_auth_table > create table {{%auth}} ... done (time: 0.016s) > add foreign key fk-auth-user_id-user-id: {{%auth}} (user_id) references {{%user}} (id) ... done (time: 0.012s) *** applied m150227_235635_create_auth_table (time: 0.033s) Migrated up successfully.
Once again, I used Yii's code generator Gii to create the Auth model:
Ultimately, the Auth table will hold contents like this:
Add the AuthChoice Widget to Meeting Planner
Yii2's AuthChoice widget does an excellent job of implementing the login buttons for each service you configure. And it does so in the order in which you set up the array of services and keys (so you can change it).
It's pretty simple to add the widget to our forms at login.php and signup.php:
<div class="row"> <div class="col-lg-5"> <p>Or, login with one of the following services:</p> <?= yii\authclient\widgets\AuthChoice::widget([ 'baseAuthUrl' => ['site/auth','mode'=>'login'], 'popupMode' => false, ]) ?> </div> <!-- end col-lg-5 --> </div> <!-- end row -->
Here's our signup page now:
For existing users who are logged in, I created an easy way for them to link their account. It's called Link Social Accounts on the profile settings page:
If you click LinkedIn, here's their OAuth screen which will ask you to give permission to Meeting Planner:
And here is the screen for Google:
But what's really happening when the user allows us to share their social account details? Let's go through the code I've written to process the users' actions.
Processing the OAuth Permission
The \frontend\controllers\SiteController.php
processes the incoming auth
action to the function onAuthSuccess
:
/** * @inheritdoc */ public function actions() { return [ 'error' => [ 'class' => 'yii\web\ErrorAction', ], 'captcha' => [ 'class' => 'yii\captcha\CaptchaAction', 'fixedVerifyCode' => YII_ENV_TEST ? 'testme' : null, ], 'auth' => [ 'class' => 'yii\authclient\AuthAction', 'successCallback' => [$this, 'onAuthSuccess'], ], ]; }
Most good OAuth clients provide similar information in a similar property array, except Twitter. Twitter was late to the game of sharing email addresses, and for my MVP it's not going to be worth the extra work to configure it now. Google and Facebook are so much more prevalent.
First, I'm gathering the service details and collecting as much personal data as I can: email, first and last name, full name, and especially the external ID of that user at that social network:
public function onAuthSuccess($client) { $mode = Yii::$app->getRequest()->getQueryParam('mode'); $attributes = $client->getUserAttributes(); $serviceId = $attributes['id']; $serviceProvider = $client->getId(); $serviceTitle = $client->getTitle(); $firstname =''; $lastname=''; $fullname =''; switch ($serviceProvider) { case 'facebook': $username = $email = $attributes['email']; $fullname = $attributes['name']; break; case 'google': $email = $attributes['emails'][0]['value']; if (isset($attributes['displayName'])) { $fullname = $username = $attributes['displayName']; } if (isset($attributes['name']['familyName']) and isset($attributes['name']['givenName'])) { $lastname = $attributes['name']['familyName']; $firstname = $attributes['name']['givenName']; } break; case 'linkedin': $username = $email = $attributes['email-address']; $lastname = $attributes['first-name']; $firstname = $attributes['last-name']; $fullname = $firstname.' '.$lastname; break; case 'twitter': $username = $attributes['screen_name']; $fullname = $attributes['name']; // to do - fix social helpers $email = $serviceId.'@twitter.com'; break; } // to do - split names into first and last with parser $auth = Auth::find()->where([ 'source' => (string)$serviceProvider, 'source_id' => (string)$serviceId, ])->one();
In the last lines of code above, we search in the Auth
table for the person's external ID. If they don't exist, they are new to Meeting Planner. If they exist, then we recognize them.
Similarly, we need to check if their email address already exists, because it's possible the person with that email address previously registered with Meeting Planner.
When there is no current authenticated user at MeetingPlanner.io, the code below looks into the incoming user data.
If the external ID is already in our Auth table, we log them in. That was easy (for them)!
If we don't recognize the ID but we have registered the email address already, we ask them to log in via user name and password and then link their account.
if (Yii::$app->user->isGuest) { if ($auth) { // if the user_id associated with this oauth login is registered, try to log them in $user_id = $auth->user_id; $person = new \common\models\User; $identity = $person->findIdentity($user_id); Yii::$app->user->login($identity); } else { // it's a new oauth id // first check if we know the email address if (isset($email) && User::find()->where(['email' => $email])->exists()) { // the email is already registered, ask person to link accounts after logging in Yii::$app->getSession()->setFlash('error', [ Yii::t('frontend', "The email in this {client} account is already registered. Please login using your username and password first, then link to this account in your profile settings.", ['client' => $serviceTitle]), ]); $this->redirect(['login']); } else { if ($mode == 'login') { // they were trying to login with an unconnected account - ask them to login normally and link after Yii::$app->getSession()->setFlash('error', [ Yii::t('frontend', "We don't recognize the user with this email from {client}. If you wish to sign up, try again below. If you wish to link {client} to your Meeting Planner account, login first with your username and password. Then visit your profile settings.", ['client' => $serviceTitle]), ]); $this->redirect(['signup']); }
Next, if they started on the login page when they clicked the social account button and we don't recognize the external ID or the email address, we redirect them to the signup page and ask them to try again—from the registration page.
If they linked from the signup page, we ensure the new user is not risking a duplicate username (of a pre-existing Meeting Planner user). In this scenario, we just extend the username with a random string for now. And we register them as a new user at Meeting Planner with a password (which they won't really need).
else if ($mode == 'signup') { // sign up a new account using oauth // look for username that exists already and differentiate it if (isset($username) && User::find()->where(['username' => $username])->exists()) { $username.=Yii::$app->security->generateRandomString(6); } $password = Yii::$app->security->generateRandomString(12); $user = new User([ 'username' => $username, // $attributes['login'], 'email' => $email, 'password' => $password, 'status' => User::STATUS_ACTIVE, ]); $user->generateAuthKey(); $user->generatePasswordResetToken(); $transaction = $user->getDb()->beginTransaction(); if ($user->save()) { $auth = new Auth([ 'user_id' => $user->id, 'source' => $serviceProvider, // $client->getId(), 'source_id' => $serviceId, // (string)$attributes['id'], ]); if ($auth->save()) { $transaction->commit(); Yii::$app->user->login($user); } else { print_r($auth->getErrors()); } } else { print_r($user->getErrors()); } } // end signup } }
In the final steps above, we add their external social account details to the Auth
table for future recognition.
Linking Existing Meeting Planner Accounts
If they are coming from the Link Social Accounts tab on the user profile page (instead of our login or signup page), then we just need to add their external account details to Auth
, and move their login to User::STATUS_ACTIVE
. (Remember, users that arrive from Meeting Planner invitation links but never registered have a User::STATUS_PASSIVE
mode.)
} else { // user already logged in, link the accounts if (!$auth) { // add auth provider $auth = new Auth([ 'user_id' => Yii::$app->user->id, 'source' => $serviceProvider, 'source_id' => $serviceId, ]); $auth->validate(); $auth->save(); $u = User::findOne(Yii::$app->user->id); $u->status = User::STATUS_ACTIVE; $u->update(); Yii::$app->session->setFlash('success', Yii::t('frontend', 'Your {serviceProvider} account has been connected to your Meeting Planner account. In the future you can log in with a single click of its logo.', array('serviceProvider'=>$serviceTitle))); } }
Here's what that looks like (in the future I'll fill in the naming information from the OAuth information—it's not done yet):
In Closing
I have to admit, the impact of working OAuth connections to major services such as Google, Facebook and LinkedIn is pretty dramatic. It makes signing up and regularly using Meeting Planner so easy and speeds future authentication. It really is kind of incredible.
Meeting Planner has really come together the last couple of months. Please try out the social login and registration right now! Watch for upcoming tutorials in our Building Your Startup With PHP series—there are lots of fun features coming up as the product heads towards MVP.
I'm also beginning to experiment with WeFunder based on the implementation of the SEC's new crowdfunding rules. Please consider following our profile. I may write about this more as part of our series.
Please feel free add your questions and comments below; I generally participate in the discussions. You can also reach me on Twitter @reifman. I welcome feature and topic requests.
If you'd like to know when the next Yii2 tutorial arrives, follow me @reifman on Twitter or check my instructor page. My instructor page will include all the articles from this series as soon as they are published.
Comments