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
'
- the
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.