Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 52 additions & 1 deletion .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,55 @@ jobs:
- name: Run test suite
run: composer run-script pre-commit
- name: Show PHP version
run: php -v
run: php -v

conformance:
name: OIDC Conformance Tests
runs-on: ubuntu-latest
env:
SUITE_BASE_URL: https://localhost.emobix.co.uk:8443
VERSION: release-v5.1.40
steps:
- uses: actions/checkout@v4
with:
path: main
- name: Setup Python Dependencies
run: |
pip install --upgrade pip
pip install httpx
- name: Conformance Suite Checkout
run: git clone --depth 1 --single-branch --branch $VERSION https://gitlab.com/openid/conformance-suite.git
- name: Conformance Suite Build
working-directory: ./conformance-suite
run: |
sed -i -e 's/localhost/localhost.emobix.co.uk/g' src/main/resources/application.properties
sed -i -e 's/-B clean/-B -DskipTests=true/g' builder-compose.yml
docker compose -f builder-compose.yml run builder
- name: Run Conformance Suite
working-directory: ./conformance-suite
run: |
docker compose -f docker-compose-dev.yml up -d
while ! curl -skfail https://localhost.emobix.co.uk:8443/api/runner/available >/dev/null; do sleep 2; done
- name: Start RP App Docker
working-directory: ./main
run: |
docker compose -f docker/docker-compose.yml up -d --build
sleep 5
- name: Start trigger-client daemon
run: |
python3 ./main/conformance-tests/trigger-client.py &
sleep 2
- name: Run Basic conformance tests
run: |
./conformance-suite/scripts/run-test-plan.py --expected-failures-file ./main/conformance-tests/basic-warnings.json --expected-skips-file ./main/conformance-tests/basic-skips.json "oidcc-client-basic-certification-test-plan[client_registration=static_client][request_type=plain_http_request]" ./main/conformance-tests/conformance-basic-ci.json
- name: Stop RP App
if: always()
working-directory: ./main
run: |
docker compose -f docker/docker-compose.yml down
- name: Stop Conformance Suite
if: always()
working-directory: ./conformance-suite
run: |
docker compose -f docker-compose-dev.yml down || true
sudo rm -rf mongo
7 changes: 7 additions & 0 deletions conformance-tests/basic-skips.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
[
{
"test-name": "oidcc-client-test-idtoken-sig-none",
"variant": "*",
"configuration-filename": "*"
}
]
1 change: 1 addition & 0 deletions conformance-tests/basic-warnings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
[]
24 changes: 24 additions & 0 deletions conformance-tests/conformance-basic-ci.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
{
"alias": "oidc-client-php",
"description": "OIDC RP conformance tests for oidc-client-php",
"client": {
"client_id": "oidc-client-php-test",
"client_secret": "oidc-client-php-test-secret",
"redirect_uri": "https://rp.local.conformance.test/callback",
"request_type": "plain_http_request"
},
"browser": [
{
"match": "https://rp.local.conformance.test*",
"tasks": [
{
"task": "Trigger RP login and wait for completion",
"match": "https://rp.local.conformance.test/",
"commands": [
["wait", "id", "submission_complete", 30]
]
}
]
}
]
}
110 changes: 110 additions & 0 deletions conformance-tests/trigger-client.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import httpx
import time
import sys
import urllib3
import urllib.parse

# Disable SSL warnings since we are using self-signed certs
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)

print("Starting trigger-client.py daemon...", flush=True)

triggered = set()

# Configure httpx clients
conformance_client = httpx.Client(verify=False)
rp_client = httpx.Client(verify=False, follow_redirects=False)

conformance_url = "https://localhost.emobix.co.uk:8443"

def trigger_rp():
url = "https://127.0.0.1/"
headers = {"Host": "rp.local.conformance.test"}
cookies = {}

# Follow up to 10 redirects manually
for redirect_num in range(10):
print(f" [Redirect {redirect_num}] Requesting: {url} (Host: {headers.get('Host')})", flush=True)
try:
resp = rp_client.get(url, headers=headers, cookies=cookies)
except Exception as e:
print(f" Request failed: {e}", flush=True)
return None

# Update cookies
for name, value in resp.cookies.items():
cookies[name] = value

if resp.status_code in (301, 302, 303, 307, 308):
redirect_url = resp.headers["Location"]
parsed = urllib.parse.urlparse(redirect_url)

if parsed.netloc == "rp.local.conformance.test":
# Rewrite to 127.0.0.1 and keep Host header
url = urllib.parse.urlunparse(parsed._replace(netloc="127.0.0.1"))
headers["Host"] = "rp.local.conformance.test"
elif parsed.netloc == "localhost.emobix.co.uk:8443":
# Keep as is, but remove Host header
url = redirect_url
if "Host" in headers:
del headers["Host"]
else:
url = redirect_url
if "Host" in headers:
del headers["Host"]

continue

# Non-redirect response, we are done
return resp

print(" Too many redirects", flush=True)
return None

# We will run this loop until we get a signal to stop or for a max timeout
start_time = time.time()
timeout = 1800 # 30 minutes max

while time.time() - start_time < timeout:
try:
# Get list of running test modules
response = conformance_client.get(f"{conformance_url}/api/runner/running")
if response.status_code != 200:
time.sleep(2)
continue

running_test_ids = response.json()
for test_id in running_test_ids:
if test_id in triggered:
continue

# Check status of this test
info_response = conformance_client.get(f"{conformance_url}/api/info/{test_id}")
if info_response.status_code != 200:
continue

info = info_response.json()
status = info.get("status")
test_name = info.get("testName")

if status == "WAITING":
print(f"Test {test_id} ({test_name}) is WAITING. Triggering RP client...", flush=True)

trigger_resp = trigger_rp()
if trigger_resp is not None:
print(f"Trigger request completed. Status: {trigger_resp.status_code}", flush=True)
if "submission_complete" in trigger_resp.text:
print("SUCCESS: submission_complete found in response!", flush=True)
else:
print("WARNING: submission_complete NOT found in response!", flush=True)
print(trigger_resp.text[:1000], flush=True) # Print first 1000 chars of response for debug
else:
print("ERROR: Trigger request failed to return a response.", flush=True)

triggered.add(test_id)
except Exception as e:
print(f"Error in polling loop: {e}", flush=True)

time.sleep(1)

print("trigger-client.py daemon stopping.", flush=True)
42 changes: 42 additions & 0 deletions docker/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
FROM php:8.2-apache

# Install system dependencies and GMP extension
RUN apt-get update && apt-get install -y \
libgmp-dev \
git \
zip \
unzip \
openssl \
&& docker-php-ext-install gmp

# Enable Apache SSL and Rewrite modules
RUN a2enmod ssl rewrite

# Generate self-signed certificate for rp.local.conformance.test
RUN openssl req -x509 -nodes -days 365 -newkey rsa:2048 \
-keyout /etc/ssl/private/rp.local.conformance.test.key \
-out /etc/ssl/certs/rp.local.conformance.test.pem \
-subj "/C=US/ST=State/L=City/O=Organization/CN=rp.local.conformance.test"

# Copy SSL site configuration
COPY docker/rp-ssl.conf /etc/apache2/sites-available/rp-ssl.conf
RUN a2ensite rp-ssl

# Set up composer
COPY --from=composer:latest /usr/bin/composer /usr/bin/composer

# Copy the application source code
WORKDIR /var/www/html
COPY . /var/www/html/

# Fix dubious ownership warning for git
RUN git config --global --add safe.directory /var/www/html

# Install PHP dependencies
RUN composer install --no-dev --prefer-dist --optimize-autoloader --ignore-platform-req=ext-xdebug

# Make sure index.php is loaded
COPY docker/rp-app/index.php /var/www/html/index.php

# Adjust permissions
RUN chown -R www-data:www-data /var/www/html
21 changes: 21 additions & 0 deletions docker/docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
services:
oidc-rp:
build:
context: ..
dockerfile: docker/Dockerfile
hostname: rp.local.conformance.test
ports:
- "443:443"
environment:
- OP_DISCOVERY_URL=https://localhost.emobix.co.uk:8443/test/a/oidc-client-php/.well-known/openid-configuration
- CLIENT_ID=oidc-client-php-test
- CLIENT_SECRET=oidc-client-php-test-secret
- REDIRECT_URI=https://rp.local.conformance.test/callback
- SCOPE=openid profile email
extra_hosts:
- "localhost.emobix.co.uk:host-gateway"

networks:
default:
name: conformance-suite_default
external: true
59 changes: 59 additions & 0 deletions docker/rp-app/index.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
<?php

declare(strict_types=1);

require_once __DIR__ . '/vendor/autoload.php';

use Cicnavi\Oidc\PreRegisteredClient;
use Cicnavi\Oidc\CodeBooks\AuthorizationRequestMethodEnum;
use GuzzleHttp\Client as GuzzleClient;

// Enable error reporting for debuggability during tests
ini_set('display_errors', '1');
ini_set('display_startup_errors', '1');
error_reporting(E_ALL);

// Read configurations from environment variables or use default test values matching the JSON config
$opConfigurationUrl = getenv('OP_DISCOVERY_URL') ?: 'https://localhost.emobix.co.uk:8443/test/a/oidc-client-php/.well-known/openid-configuration';
$clientId = getenv('CLIENT_ID') ?: 'oidc-client-php-test';
$clientSecret = getenv('CLIENT_SECRET') ?: 'oidc-client-php-test-secret';
$redirectUri = getenv('REDIRECT_URI') ?: 'https://rp.local.conformance.test/callback';
$scope = getenv('SCOPE') ?: 'openid';

try {
// Disable SSL verification for internal Guzzle client because conformance-suite uses a self-signed cert
$httpClient = new GuzzleClient(['verify' => false]);

$client = new PreRegisteredClient(
opConfigurationUrl: $opConfigurationUrl,
clientId: $clientId,
clientSecret: $clientSecret,
redirectUri: $redirectUri,
scope: $scope,
httpClient: $httpClient,
defaultAuthorizationRequestMethod: AuthorizationRequestMethodEnum::Query
);

$path = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);

if ($path === '/callback') {
// Exchange authorization code for token and fetch user data
$userData = $client->getUserData();

// Print success div for automated browser/curl matching
echo '<html><head><title>OIDC RP Test Completion</title></head><body>';
echo '<div id="submission_complete">OIDC Flow Successful!</div>';
echo '<h1>User Data</h1><pre>' . htmlspecialchars(json_encode($userData, JSON_PRETTY_PRINT)) . '</pre>';
echo '</body></html>';
} else {
// Initiate OIDC authorization flow
$client->authorize();
}
} catch (\Throwable $e) {
http_response_code(500);
echo '<html><head><title>OIDC RP Test Error</title></head><body>';
echo '<h1>Error</h1>';
echo '<pre>' . htmlspecialchars($e->getMessage()) . "\n" . htmlspecialchars($e->getTraceAsString()) . '</pre>';
echo '</body></html>';
exit;
}
18 changes: 18 additions & 0 deletions docker/rp-ssl.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<VirtualHost *:443>
ServerName rp.local.conformance.test
DocumentRoot /var/www/html

SSLEngine on
SSLCertificateFile /etc/ssl/certs/rp.local.conformance.test.pem
SSLCertificateKeyFile /etc/ssl/private/rp.local.conformance.test.key

<Directory /var/www/html>
AllowOverride All
Require all granted
FallbackResource /index.php
</Directory>

# Log configuration
ErrorLog ${APACHE_LOG_DIR}/error.log
CustomLog ${APACHE_LOG_DIR}/access.log combined
</VirtualHost>
1 change: 1 addition & 0 deletions docs/1-Index.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ passed by value.
Check the dedicated sections below for more details about each client type:
* [Pre-registered Client](2-Pre-Registered-Client.md)
* [Federated Client](3-Federated-Client.md)
* [Conformance Testing](4-Conformance-Testing.md)


## Note on SameSite Cookie Attribute
Expand Down
Loading
Loading