Stormpath Logo
  • Blog

The Stormpath API shut down on August 17, 2017. Thank you to all the developers who have used Stormpath.

Tutorial: Social Login for PHP with Stormpath & ID Site

by Brian Retterer | September 28, 2016 |

  • PHP

Social login and registration are hot features that have become “expected” of most new applications. Building these features can be difficult and fraught, and ultimately something most developers aren’t excited to tackle.

Here’s the good news: Social login is a core feature of the Stormpath PHP SDK! With Stormpath, you only need a few lines of code to build simple, robust social login and registration support for the four major social login providers: Facebook, Google, LinkedIn, and GitHub.

At Stormpath, we have two ways of using social providers and in this tutorial we are going to cover how you can use ID Site for authentication via social login in PHP. I will then convert that ID Site authorization into JWT cookie-based authentication that you can use for the remainder of the authenticated session. With Stormpath and ID Site the process for using each social provider is roughly the same. We’ll use Google in this tutorial, but you can find granular setup instructions for each provider in our ID Site documentation.

Setup Your Google and Stormpath Applications

This tutorial assumes you have some basic knowledge of setting up an application and account inside of Stormpath as well as working with the Google API Manager, so I will only hit on the key points of both. You can find more information on all the providers we support over on our product guide.

Create an Application with Google

After signing up for an account at Stormpath and saving your API keys, we need to go to Google and set up a new application. You need to create credentials for OAuth client ID. This can be done by visiting the Google Developer Console. Select Web Application and entering the data below.

NOTE: This tutorial uses Laravel Valet to serve local projects at the .dev TLD. If you do not have Laravel Valet, and you are on a Mac, I recommend checking it out as it makes local development a lot easier. The URL I’ll be using for this tutorial is http://stormpathsocial.dev/.

Since we plan to use ID Site, you will have to get your ID Site URL from Stormpath and place it in the Authorized JavaScript origins. Go to the ID Site Dashboard and copy the Domain Section. Mine is formal-ring.id.stormpath.io so I will fill out the field in google as https://formal-ring.id.stormpath.io

Stormpath ID Site Domain Name

The other area that you need to make sure is correct is the Authorized Redirect URIs section. This has to match exactly with the URI that you will send the authentication back to after Google handles it. For the following tutorial, we will use http://stormpathsocial.dev/handleCallback.php?provider=google

Google OAuth client ID Setup

NOTE: Adding the ?provider=google is not required. I do that if I plan to use other providers, so I can have a single handler for all social providers.

Once done, click Create and your keys will be displayed. Make sure you copy these down as we will need them next.

Google OAuth ID and Secret

Prepare your Stormpath Account

If you don’t currently have an account at Stormpath, go ahead and register now, I’ll wait here until you are back…

… Ok, now that you have an account at Stormpath, let’s log in and get some API keys to use for our project. From the dashboard, click on either Create API Keys if you are using a new account, or Manage API Keys and then Create API Keys if you had an existing account. This will download a file that we will be using later.

Set Up Api Keys

Next, let’s set up our Google Directory. Go to the directories page and Create Directory. Fill out the fields including your ID and Secret you received from Google during the creation of the OAuth credentials. You then need to make sure to use the same Authorized Redirect URI that you used when creating the credentials at Google, http://stormpathsocial.dev/handleCallback.php?provider=google.
Having a directory that is not mapped to an application is not going to do us much good, so let’s do that. Go your applications screen in Stormpath click the application name you want to map the directory to, and then Account Stores. Here you can click Add Account Store and select the directory you just added.

NOTE: By default, you have two applications, My Application and Stormpath. You will want to use My Application as Stormpath is the one used for administration of your account.

To use ID Site for your social login, you will need to do a little customization of the settings in Stormpath’s ID Site Settings. There are two fields here that need to be updated. The first is the Authorized Javascript Origin URLs. For this tutorial, we are going to use http://stormpathsocial.dev. The next field is the Authorized Redirect URLs where we will put in http://stormpathsocial.dev/handleCallback.php. We are leaving off the provider query param as we will handle that differently this time.

Prep Your Code Base

Since this is a clean project, I will go through each step of the project. I store all of my projects at ~/Code so in that directory, let’s create a directory StormpathSocial. Since I am using Laravel Valet, as soon as that directory is created, I can go to http://stormpathsocial.dev.

We will be using a few packages, so let’s setup a new composer.json file with the following:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
{
    "name": "bretterer/stormpath-social",
    "description": "Social Login with Stormpath",
    "type": "Project",
    "require": {
        "stormpath/sdk": "^1.16",
        "vlucas/phpdotenv": "^2.4",
        "symfony/http-foundation": "^3.1"
    },
    "autoload": {
        "files": [
            "helpers.php"
        ]
    },
    "license": "Apache-2.0",
    "authors": [
        {
            "name": "Brian Retterer",
            "email": "[email protected]"
        }
    ]
}
 

Before running composer install make sure you create an empty file helpers.php in the root of the project. This file will contain some helper functions we will use along the way. The other packages we are using are of course the Stormpath PHP SDK for all communication with the Stormpath API. We will also be using vlucas/phpdotenv so we can set up some environment variables along with symfony/http-foundation for some functionality of setting cookies and responding with redirects.

Once the composer install is complete, we can start developing some code. Start by creating a bootstrap.php file that we will use for setting up the Stormpath Client. This will also be where we autoload our vendor file.

/bootstrap.php

1
2
3
4
5
6
7
require_once __DIR__ . '/vendor/autoload.php';
 
// Load our Env file
$dotenv = new Dotenv\Dotenv(__DIR__);
$dotenv->load();
 
 

Having this bootstrap file allows us to create our .env file to store all of our keys. The data for the Stormpath keys can be found in the file that was downloaded when you created the new API Keys. The application can be found on the application page where you mapped the directory. For the Google ID and secret, you will need to reference the keys from the developer console

/.env

1
2
3
4
5
6
7
STORMPATH_CLIENT_APIKEY_ID=1B8IKPVQ66PQEJ06G3X2ZIN0A
STORMPATH_CLIENT_APIKEY_SECRET=iSAvJozGbMVQReBKxoQSmHNYAEFzGf/QTDJCWtQ5bqo
STORMPATH_APPLICATION_HREF=https://api.stormpath.com/v1/applications/16k5PC57Imx4nWXQXi74HO
 
GOOGLE_APP_ID=1044707546568-tdmcis68j0g9qg0eh6qi8r99e045gb3u.apps.googleusercontent.com
GOOGLE_APP_SECRET=ap6CmtdVxQpa11YdwKMi_Bm5
 

Setup Templates with Bootstrap

To give this project a little bit of style, we are going to pull in Bootstrap. There are some parts of the template that will be used across the different pages we have, so let’s create a _partials directory that will store the header, navigation, and footer.

/_partials/head.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<!doctype html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport"
          content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Stormpath Social Example</title>
 
    <link href="https://maxcdn.bootstrapcdn.com/bootswatch/3.3.7/darkly/bootstrap.min.css" rel="stylesheet">
 
</head>
<body>
 

/_partials/nav.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
<nav class="navbar navbar-default">
    <div class="container-fluid">
        <div class="navbar-header">
            <button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#bs-example-navbar-collapse-1">
                <span class="sr-only">Toggle navigation</span>
                <span class="icon-bar"></span>
                <span class="icon-bar"></span>
                <span class="icon-bar"></span>
            </button>
            <a class="navbar-brand" href="/">Stormpath Social Example (Id Site)</a>
        </div>
 
        <div class="collapse navbar-collapse" id="bs-example-navbar-collapse-1">
            <?php if(null === $user) : ?>
                <ul class="nav navbar-nav navbar-right">
                    <li><a href="/login.php">Login</a></li>
                    <li><a href="register.php">Register</a></li>
                </ul>
            <?php else: ?>
                <ul class="nav navbar-nav navbar-right">
                    <li class="dropdown">
                        <a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-expanded="false"><?php print $user->givenName . ' ' . $user->surname . ' ( ' . $user->email . ' ) '; ?><span class="caret"></span></a>
                        <ul class="dropdown-menu" role="menu">
                            <li class="divider"></li>
                            <li><a href="logout.php">Logout</a></li>
                        </ul>
                    </li>
                </ul>
            <?php endif; ?>
        </div>
    </div>
</nav>
 

/_partials/footer.php

1
2
3
4
5
    <script   src="https://code.jquery.com/jquery-3.1.0.min.js"   integrity="sha256-cCueBR6CsyA4/9szpPfrX3s49M9vUU5BgtiJj06wt/s="   crossorigin="anonymous"></script>
    <script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js" integrity="sha384-Tc5IQib027qvyjSMfHjOMaLkfuWVxZxUPnCJA7l2mCWNIpG9mGCD8wGNIcPD7Txa" crossorigin="anonymous"></script>
</body>
</html>
 

To use these in the template files, we will utilize our helper.php file. Lets create a few methods in there:

/helpers.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
/**
* Require the footer template
*/
function getFooter()
{
    require __DIR__ . '/_partials/footer.php';
}
 
/**
* Require the head template
*/
function getHead()
{
    require __DIR__ . '/_partials/head.php';
}
 
/**
* Require the navigation.
*
* @param $user
*/
function getNav($user = null)
{
    require __DIR__ . '/_partials/nav.php';
}
 

Now we create our index.php file and can use the template partials.

/index.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?php
    require __DIR__ . '/bootstrap.php';
 
    getHead();
    getNav();
?>
 
<div class="container">
    <div class="well">
        <h2>Stormpath Social Login Example (ID Site)</h2>
        <p>
            This example is meant to show you the steps to building social login with the PHP SDK
        </p>
    </div>
</div>
 
<?php getFooter(); ?>
 

Build your Bootstrap File

Now we can begin adding the Social Login flow. The first thing we need to do is create a Stormpath Client. We will do this inside of our bootstrap.php file. We are going to build the client with the ClientBuilder class and set the API keys manually from the .env variable. Open the bootstrap file and after the loading of the Dotenv add the following.

1
2
3
4
5
6
7
// Create a Stormpath Client
/** @var \Stormpath\ClientBuilder $clientBuilder */
$clientBuilder = new \Stormpath\ClientBuilder();
$clientBuilder->setApiKeyProperties("apiKey.id=".getenv('STORMPATH_CLIENT_APIKEY_ID')."\napiKey.secret=".getenv('STORMPATH_CLIENT_APIKEY_SECRET'));
/** @var \Stormpath\Client $client */
$client = $clientBuilder->build();
 

This will pull the API keys from the environment variables and create a valid ini string to pass to the client builder. Once that is done, we build the client and set it to the variable $client that will be usable in our application.

We now need to get the application we will be using for login. while in the same Bootstrap file we need to get the application resource by adding these lines below the previous:

1
2
3
4
// Get the Stormpath Application
/** @var \Stormpath\Resource\Application $application */
$application = $client->getDataStore()->getResource(getenv('STORMPATH_APPLICATION_HREF'), \Stormpath\Stormpath::APPLICATION);
 

The last item in the Bootstrap file that you need to add is getting the current user if there is one logged in. Since we will be using cookies to store the access_tokens, we can look there for a valid user.

1
2
3
4
5
6
7
8
9
10
// Get the User if found
$user = null;
if(request()->cookies->has('access_token')) {
    try {
        $decoded = JWT::decode(request()->cookies->get('access_token'), getenv('STORMPATH_CLIENT_APIKEY_SECRET'), ['HS256']);
        $user = $client->getDataStore()->getResource($decoded->sub, \Stormpath\Stormpath::ACCOUNT);
    } catch (\Stormpath\Resource\ResourceError $re) {
        die($re->getMessage());
    }
 

This block of code sets a user variable to null so we have something to work with in the navigation if there is no user. We then look to see if the request cookies have an access_token. If a cookie exists with that name, we will decode it with the JWT library. We can use our API key secret to validate the integrity of the token, and only allow signing algorithms of HS256 since that is what Stormpath uses to sign the token. If a user exists, we update the user variable with the account object from the Stormpath SDK.

If we run into any errors, we let it be known by dying with the error message. Let’s clean this up a little bit, though. Instead of the method die(), let’s change that to error() and create the following function inside of our helpers.php file

1
2
3
4
5
6
7
8
9
10
/**
* Prints a pre formatted error message
*
* @param $message
*/
function error($message)
{
    print "<pre>ERROR: {$message}</pre>";
}
 

Log in with Google

Now that we have all of our application Bootstrap setup done, we can now log into the Stormpath Google Directory of our application. If you take a look at your _partials/nav.php file, you can see that our login link takes us to login.php, so let’s create that now.

You might be thinking at this point that this is where all the Stormpath code comes into play. I can tell you (and show you) that you can create a login script with only four lines of code.

/login.php

1
2
3
4
5
6
7
8
use Symfony\Component\HttpFoundation\Response;
 
require_once __DIR__ . '/bootstrap.php';
 
$url = $application->createIdSiteUrl(['callbackUri' => 'http://stormpathsocial.dev/handleCallback.php']);
 
$response = Response::create('', Response::HTTP_FOUND, ['Location' => $url])->send();
 

That’s it.

This script line by line says, I want to use the Symfony Response class, which we will use at the end to redirect the user. I then want to use my bootstrap.php file to gain access to some variables we set there.

(This next part is where the magic happens, watch closely!)

Looking at the next line of the script, I want to create an ID Site URL from the Stormpath application object that I can redirect the user to. For our purposes, I need to set a callbackUri, the URI the user will be redirected back to after login is complete.

Finally, I create a response with a header parameter along with a response code of HTTP_FOUND (302) and send it. The header parameter we use, Location, along with the response code triggers the browser to issue a redirect to the URL we specify. This will send the user to ID Site where they will see a login screen with a google button.

ID Site Login Window

And there you have it. Pretty cool, right?

How to Handle Google Sign in Callback

Once the user logs in here, they will be redirected back the callbackUri which we need to create now. If you did not want to use access tokens for authentication, this could be done in a single line of code, but what’s the fun in that? Let’s make our site a little more secure and use cookie-based authentication.

We will need to create a handleCallback.php file in the root of our project where we will do the conversion of the ID Site token to access tokens.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
require_once __DIR__ . '/bootstrap.php';
 
use Symfony\Component\HttpFoundation\Cookie;
use Symfony\Component\HttpFoundation\Response;
 
$exchangeIdSiteTokenRequest = new \Stormpath\Oauth\ExchangeIdSiteTokenRequest(request()->get('jwtResponse'));
$auth = new \Stormpath\Oauth\ExchangeIdSiteTokenAuthenticator($application);
$result = $auth->authenticate($exchangeIdSiteTokenRequest);
 
$accessToken = new Cookie("access_token", $result->getAccessTokenString(), time()+3600, '/', 'stormpathsocial.dev');
$refreshToken = new Cookie("refresh_token", $result->getRefreshTokenString(), time()+3600, '/', 'stormpathsocial.dev');
$response = Response::create('', Response::HTTP_FOUND, ['Location' => '/']);
$response->headers->setCookie($accessToken);
$response->headers->setCookie($refreshToken);
 
$response->send();
 

This file, minus the HTML files, will be the longest file you have to deal with for full authentication in your web application. Most of the file is actually just handling setting the cookies before you redirect them back to the home page.

The first block of code after the typical require and use statements deals with exchanging the ID Site token with the access tokens. This is where the core of the work is done. You will see a new function here, request(). We will need to add this to our helpers.php file. This function is a nice way of saying “get the requested object.”

1
2
3
4
5
6
7
8
9
10
/**
* Get an instance of the Request object
*
* @return \Symfony\Component\HttpFoundation\Request
*/
function request()
{
    return \Symfony\Component\HttpFoundation\Request::createFromGlobals();
}
 

Once we have this function, we can now access the query parameters without using the insecure PHP global $_GET or $_REQUEST.

PROTIP: I suggest that anytime you want to use a superglobal like this, you should use a package that is designed around superglobals but does not directly use them. Symfony has the best ones in my opinion and has becomethe industry standard.

Going back to the code sample, we are going to get the property jwtRespose which is a JSON web token that defines the user who just logged in. This is why I say that you could stop here as you have all the information on the user you need. We, however, are going to keep going and send that token through the \Stormpath\Oauth\ExchangeIdSiteTokenRequest class. We then authenticate against the application using the \Stormpath\Oauth\ExchangeIdSiteTokenAuthenticator passing in the application and finally the token request.

On a successful response, we will receive an access token, and a refresh token. Taking the string of both of theseJSON Web Tokens, we will store them in the cookies. For this demo, we are just using the access_token so I have set them both to expire in 3600 seconds, however, you could get the token settings from the directory to see the defined expired times and set them based on that. See the Token management section of our documentation for more information.

Once the cookie objects are created, create a response object to redirect the user back home and set the cookies on the response. After sending the response to the browser, the user is directed back home and they will be logged into the site.

Logged Into Applicaiton

Social Authentication — Logging Out

Logging out while using ID Site can be done by just clearing the cookies you set, but let’s take it one step further by clearing out the tokens from Stormpath as well to prevent someone logging in with the same cookie. Create a logout.php file where we will clear the cookies, and then send delete requests to Stormpath for both the access_token and refresh_token.

logout.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
use Symfony\Component\HttpFoundation\Cookie;
use Symfony\Component\HttpFoundation\Response;
 
require_once __DIR__ . '/bootstrap.php';
 
if(request()->cookies->has('access_token')) {
    $decoded = JWT::decode(request()->cookies->get('access_token'), getenv('STORMPATH_CLIENT_APIKEY_SECRET'), ['HS256']);
    $client->getDataStore()->getResource('/accessTokens/'.$decoded->jti, \Stormpath\Stormpath::ACCESS_TOKEN)->delete();
 
}
 
if(request()->cookies->has('refresh_token')) {
    $decoded = JWT::decode(request()->cookies->get('refresh_token'), getenv('STORMPATH_CLIENT_APIKEY_SECRET'), ['HS256']);
    $client->getDataStore()->getResource('/refreshTokens/'.$decoded->jti, \Stormpath\Stormpath::REFRESH_TOKEN)->delete();
}
 
$accessToken = new Cookie("access_token", 'expired', time()-4200, '/', 'stormpathsocial.dev');
$refreshToken = new Cookie("refresh_token", 'expired', time()-4200, '/', 'stormpathsocial.dev');
$response = Response::create('', Response::HTTP_FOUND, ['Location' => '/']);
$response->headers->setCookie($accessToken);
$response->headers->setCookie($refreshToken);
 
$response->send();
 

The thing you may notice here is the way I expire the cookies. I am setting up new cookies with the same names, but doing two different things here. The first is setting the value to expired. This is to make sure the JWT is cleared out of the cookie and should trigger to developers that this cookie should be expired and not used if setting the time does not work. For the time, I get the current time and subtract 4200, which should be plenty of time in the past to tell the browser to get rid of the cookie. 4200 is arbitrarily selected, feel free to choose your own favorite number of seconds.

NOTE: There is a way to create a URI through ID Site for logging out that you may want to use instead or in parallel with the method above. Using the method above will not log out you of ID Site so there is a chance that when the user goes to log in again, they will not see the ID site login screen. Logging out will be done and redirect you back to the same handleCallback page. This means you will have to do a switch on the type of token based on the JWT status. For information on this, visit Using ID Site in the docs.

Register A User with Google Social

Registration is very similar to the process for logging in. If you think about it, you are just logging into your application with a Google account. Stormpath handles the creation of the account if it does not already exist inside of your application. I provided a registration link just to show how it is done. On the register.php page that you create, you will add the same code as login.php with one minor addition during the createIdSiteUri method.

register.php

1
2
3
4
5
6
7
8
use Symfony\Component\HttpFoundation\Response;
 
require_once __DIR__ . '/bootstrap.php';
 
$url = $application->createIdSiteUrl(['callbackUri' => 'http://stormpathsocial.dev/handleCallback.php', 'path'=>'/#register']);
 
$response = Response::create('', Response::HTTP_FOUND, ['Location' => $url])->send();
 

The path as part of the array in createIdSiteUrl tells ID Site that you want to see the registration page instead of the login page. The rest will be the exact same.

Learn More!

If you have made it this far, you now understand the basics of using ID site for social authentication. The methods are the same for any of our social providers you want to use. As a bonus, the flow is the same for any SAML provider that you want to add to your site. Here are some resources that you can use to learn more about what you just read.

  • ID Site
  • ID Site Documentation
  • PHP SDK
  • PHP SDK DOCS
  • EXAMPLE CODE

If you have any questions or comments about this tutorial, please feel free to reach out to us at support@stormpath.com. Follow me on twitter @bretterer.

// Brian

?>

Explore the Topic

  • .NET
  • General
  • Java
  • Javascript
  • Mobile
  • Node
  • PHP
  • Python
  • REST API

Share a Post

0
0
0
0
0
0
0
0
Support: [email protected]
Copyright 2017 Stormpath