While working on some internal tools, I needed secure access to a few PHP pages and virtual directories resources that, by default, didn’t have any built-in access control. Since NetEye already uses Keycloak as its authentication system, I decided to leverage it to handle login and user validation.
This way I could avoid reinventing the wheel and rely on a centralized, robust identity provider that was already part of the infrastructure.
Here’s how I implemented two different setups:
mod_auth_openidc
Before diving into the two methods, you’ll need the client secret for the client “neteye” in Keycloak. This is required whether you’re configuring Apache or writing custom PHP code.
In the screenshots below, I’ll show you exactly where to find it.
First, navigate to the Keycloak admin console:
Select the realm used by NetEye (usually “Neteye master”), go to Clients and select “neteye”:
Under the Credentials tab, you’ll find the client secret:
Make sure to copy it securely, you’ll need it in both configuration scenarios below.
mod_auth_openidc
for Virtual DirectoriesThis method is ideal if your application is served by Apache and you want to protect a full path without modifying the app itself.
mod_auth_openidc
module installed:dnf install mod_auth_openidc
Here’s the configuration I used to secure “/test”:
cd /etc/httpd/conf.d
vim test.conf
Edit the file as shown below, and make sure to replace:
LoadModule auth_openidc_module modules/mod_auth_openidc.so
OIDCProviderMetadataURL https://<YOUR_FQDN>/auth/realms/master/.well-known/openid-configuration
OIDCClientID neteye
OIDCClientSecret <YOUR_CLIENT_SECRET>
OIDCRedirectURI https://<YOUR_FQDN>/test/redirect_uri
OIDCCryptoPassphrase a1b2c3d4e5f6g7h8i9j0strongpassphrase
OIDCSSLValidateServer Off
OIDCStateTimeout 600
<Location /test>
AuthType openid-connect
Require valid-user
</Location>
ProxyPass /test https://<YOUR_FQDN>:<YOUR_PORT>/test/
ProxyPassReverse /test https://<YOUR_FQDN>:<YOUR_PORT>/test/
SSLProxyEngine On
SSLProxyVerify none
SSLProxyCheckPeerCN off
SSLProxyCheckPeerName off
In the Keycloak admin console (already configured by NetEye):
neteye"
client/test/*
Once this is in place, any access to
/test
will trigger Keycloak authentication. No need to modify the application itself.
For standalone PHP pages that don’t have any built-in access control, I created a custom authentication flow using Keycloak’s endpoints.
auth.php
– Handles AuthenticationThis script manages the full OpenID Connect flow:
You can include this at the top of any PHP page to enforce login:
<?php
session_start();
set_time_limit(300);
$kcBase = 'https://<YOUR_FQDN>/auth/realms/master';
$authEndpoint = $kcBase . '/protocol/openid-connect/auth';
$tokenEndpoint = $kcBase . '/protocol/openid-connect/token';
$clientId = 'neteye';
$clientSecret = '<YOUR_CLIENT_SECRET>';
$scope = 'openid profile email';
$scriptPath = $_SERVER['PHP_SELF'];
$redirectUri = 'https://' . $_SERVER['HTTP_HOST'] . $scriptPath
. (empty($_SERVER['QUERY_STRING']) ? '' : '?' . $_SERVER['QUERY_STRING']);
if (empty($_SESSION['access_token'])) {
if (isset($_GET['code'])) {
if (empty($_GET['state'])
|| ! isset($_SESSION['oauth2state'])
|| $_GET['state'] !== $_SESSION['oauth2state']
) {
unset($_SESSION['oauth2state']);
die('Invalid state');
}
$post = [
'grant_type' => 'authorization_code',
'code' => $_GET['code'],
'redirect_uri' => $redirectUri,
];
$ch = curl_init($tokenEndpoint);
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => http_build_query($post),
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HEADER => true,
CURLOPT_HTTPHEADER => [
'Accept: application/json',
'Authorization: Basic ' . base64_encode("$clientId:$clientSecret")
],
CURLOPT_SSL_VERIFYPEER => false,
CURLOPT_SSL_VERIFYHOST => 0,
]);
$resp = curl_exec($ch);
if ($resp === false) {
die('CURL error: ' . curl_error($ch));
}
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$hdrSize = curl_getinfo($ch, CURLINFO_HEADER_SIZE);
$header = substr($resp, 0, $hdrSize);
$body = substr($resp, $hdrSize);
curl_close($ch);
if ($httpCode !== 200) {
header('Content-Type: text/plain; charset=utf-8');
echo "HTTP_CODE: $httpCode\n=== HEADER ===\n$header\n=== BODY ===\n$body";
exit;
}
$data = json_decode($body, true);
if (empty($data['access_token'])) {
die('Nessun access_token ricevuto');
}
$_SESSION['access_token'] = $data['access_token'];
// rimuovo code e state dalla querystring
header('Location: ' . $redirectUri);
exit;
}
$state = bin2hex(random_bytes(16));
$_SESSION['oauth2state'] = $state;
$params = [
'response_type' => 'code',
'client_id' => $clientId,
'redirect_uri' => $redirectUri,
'scope' => $scope,
'state' => $state,
];
header('Location: ' . $authEndpoint . '?' . http_build_query($params));
exit;
}
protected_page.php
– A Generic Protected PageHere’s a simple example of a PHP page that doesn’t execute any commands or require parameters. It just displays hard-coded, static content to authenticated users.
<?php
require __DIR__ . '/auth.php';
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Protected Page</title>
<style>
body {
font-family: sans-serif;
margin: 2em;
}
.container {
max-width: 600px;
margin: auto;
}
</style>
</head>
<body>
<div class="container">
<h1>Welcome to the Protected Page</h1>
<p>You are successfully authenticated via Keycloak.</p>
<p>This page is only accessible to users with valid credentials.</p>
</div>
</body>
</html>
Integrating Keycloak into my PHP pages and virtual directories turned out to be a clean and effective way to enforce authentication – especially since NetEye already uses it as its identity provider. Instead of building a custom login system or relying on basic .htaccess rules, I was able to hook into a centralized, secure, and scalable solution.
Whether you’re protecting an entire Apache-served directory or a single PHP page, the two approaches I described are flexible enough to cover most use cases:
Did you find this article interesting? Does it match your skill set? Programming is at the heart of how we develop customized solutions. In fact, we’re currently hiring for roles just like this and others here at Würth Phoenix.