As we've seen in a previous post, you can install your own SSO, using Keycloak.

In this post, we'll see how you can connect to that SSO using only one page of PHP.

The dependencies

Install these composer packages:

composer install stevenmaguire/oauth2-keycloak
composer install firebase/php-jwt

The code

Put this in your index.php at the root of your publicly-available web folder:

<?php

require 'vendor/autoload.php';

use Stevenmaguire\OAuth2\Client\Provider\Keycloak;
use Firebase\JWT\JWT;
use Firebase\JWT\JWK;

// error_reporting(E_ALL);
// ini_set('display_errors', 1);

ini_set('session.gc_maxlifetime', 1440);
ini_set('session.cookie_lifetime', 0);
ini_set('session.cookie_secure', 1);
ini_set('session.cookie_httponly', 1);

session_start();

$keycloak_base_url = 'https://sso.yourdomain.com';
$realm_slug = 'family';
$jwksUrl = "$keycloak_base_url/realms/$realm_slug/protocol/openid-connect/certs";
$provider = new Keycloak([
    'authServerUrl'         => $keycloak_base_url,
    'realm'                 => $realm_slug,
    'clientId'              => 'yourclient_id',
    'clientSecret'          => '[your secret here]',
    'redirectUri'           => 'https://yourdomain.com/index.php',
    'encryptionAlgorithm'   => 'RS256',                                       // optional
    'encryptionKeyPath'     => '/some/path/to/your_keycloak_privatekey.pem',  // optional
    'encryptionKey'         => 'contents_of_key_or_certificate',              // optional
]);

if (isset($_GET['action']) && $_GET['action'] === 'logout') {
  unset($_SESSION['access_token']);
  unset($_SESSION['decoded_jwt']);
  header('Location: /index.php');
}

if (!isset($_GET['code'])) {

    // If we don't have an authorization code then get one
    $authUrl = $provider->getAuthorizationUrl();
    $_SESSION['oauth2state'] = $provider->getState();
    header('Location: '.$authUrl);
    exit;

// Check given state against previously stored one to mitigate CSRF attack
} elseif (empty($_GET['state']) || ($_GET['state'] !== ($_SESSION['oauth2state'] ?? ''))) {

    unset($_SESSION['oauth2state']);
    exit('Invalid state, make sure HTTP sessions are enabled.');

} else {

    // Try to get an access token (using the authorization coe grant)
    try {
        $token = $provider->getAccessToken('authorization_code', [
            'code' => $_GET['code']
        ]);
    } catch (Exception $e) {
        exit('Failed to get access token: '.$e->getMessage());
    }

    // Optional: Now you have a token you can look up a users profile data
    try {

        // We got an access token, let's now get the user's details
        $user = $provider->getResourceOwner($token);

        // Use these details to create a new profile
        printf('Hello %s!', $user->getName());

    } catch (Exception $e) {
        exit('Failed to get resource owner: '.$e->getMessage());
    }

    // Use this to interact with an API on the users behalf
    $_SESSION['access_token'] = $token->getToken();


    $token = $_SESSION['access_token'] ?? null;

    if (!$token) {
        die("No access token found. Please log in.");
    }

    // Fetch Keycloak's public key (JWKS)
    $jwks = json_decode(file_get_contents($jwksUrl), true);
    $keys = JWK::parseKeySet($jwks);

    // Decode & Verify the JWT
    try {
        $decoded = JWT::decode($token, $keys);
        $_SESSION['decoded_jwt'] = $decoded;
        echo "<pre>";
        print_r($decoded); // Token data
    } catch (Exception $e) {
        die("Invalid token: " . $e->getMessage());
    }
}

Make sure you set your own:

  • $realm_slug
  • $keycloak_base_url
  • in $provider object:
    • the 'redirectUri' property
    • the 'clientId' property
    • the 'redirectUri' property
    • the 'redirectUri' property
    • (optional) the 'encryptionAlgorithm'
    • (optional) the 'encryptionKeyPath'
    • (optional) the 'encryptionKey'

Where to go from here

  • As you can see, we don't persist the token we get from the OAuth2 service. So you might want to add some session + cookie persistence in there.
  • You'll also want to do something else than simply print out the content of your JWT in your page.
  • And add proper error logging instead of displaying everything to the users.