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
3 changes: 3 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Normalize line endings: store all text files with LF in the repository
# and use LF in working trees, regardless of platform.
* text=auto eol=lf
26 changes: 26 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,32 @@ jobs:
- name: Run Basic conformance tests with dynamic client registration
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=dynamic_client][request_type=plain_http_request]" ./main/conformance-tests/conformance-basic-dynamic-ci.json
- name: Switch RP App to RP-Initiated Logout flow
working-directory: ./main
run: |
LOGOUT_FLOW=rp_initiated docker compose -f docker/docker-compose.yml up -d
sleep 5
- name: Restart trigger-client daemon for RP-Initiated Logout tests
run: |
pkill -f trigger-client.py || true
python3 ./main/conformance-tests/trigger-client.py &
sleep 2
- name: Run RP-Initiated Logout conformance tests
run: |
./conformance-suite/scripts/run-test-plan.py --expected-failures-file ./main/conformance-tests/basic-warnings.json "oidcc-client-rp-initiated-logout-rp-basic[client_auth_type=client_secret_basic][client_registration=static_client][request_type=plain_http_request]" ./main/conformance-tests/conformance-rp-logout-ci.json
- name: Switch RP App to RP-Initiated Logout flow with dynamic client registration
working-directory: ./main
run: |
CLIENT_REGISTRATION=dynamic_client LOGOUT_FLOW=rp_initiated docker compose -f docker/docker-compose.yml up -d
sleep 5
- name: Restart trigger-client daemon for dynamic RP-Initiated Logout tests
run: |
pkill -f trigger-client.py || true
python3 ./main/conformance-tests/trigger-client.py &
sleep 2
- name: Run RP-Initiated Logout conformance tests with dynamic client registration
run: |
./conformance-suite/scripts/run-test-plan.py --expected-failures-file ./main/conformance-tests/basic-warnings.json "oidcc-client-rp-initiated-logout-rp-basic[client_auth_type=client_secret_basic][client_registration=dynamic_client][request_type=plain_http_request]" ./main/conformance-tests/conformance-rp-logout-dynamic-ci.json
- name: Stop RP App
if: always()
working-directory: ./main
Expand Down
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,5 @@
composer.lock
/.idea/
/.scannerwork/
sonar-project.properties
sonar-project.properties
__pycache__/
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@ Generic OpenID Connect (OIDC) client (RP) written in PHP.
It uses OIDC Authorization Code Flow to perform authentication. It implements
JWKS public key usage and automatic key rollover, caching mechanism
(file based by default), ID token verification and claims extraction,
'userinfo' user data fetching and has support for automatic client
registration for federated environments, as well as OpenID Connect Dynamic
Client Registration.
'userinfo' user data fetching, OIDC RP-Initiated Logout, and has support for
automatic client registration for federated environments, as well as OpenID
Connect Dynamic Client Registration.

For information on how to use this client, refer to the
[documentation](docs/1-Index.md).
28 changes: 28 additions & 0 deletions conformance-tests/conformance-rp-logout-ci.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
{
"alias": "oidc-client-php",
"description": "OIDC RP-Initiated Logout 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",
"post_logout_redirect_uris": [
"https://rp.local.conformance.test/logout-callback"
],
"backchannel_logout_uri": "https://rp.local.conformance.test/backchannel-logout",
"request_type": "plain_http_request"
},
"browser": [
{
"match": "https://rp.local.conformance.test*",
"tasks": [
{
"task": "Trigger RP login with logout and wait for completion",
"match": "https://rp.local.conformance.test/",
"commands": [
["wait", "id", "submission_complete", 30]
]
}
]
}
]
}
18 changes: 18 additions & 0 deletions conformance-tests/conformance-rp-logout-dynamic-ci.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"alias": "oidc-client-php",
"description": "OIDC RP-Initiated Logout conformance tests for oidc-client-php (dynamic client registration)",
"browser": [
{
"match": "https://rp.local.conformance.test*",
"tasks": [
{
"task": "Trigger RP login with logout and wait for completion",
"match": "https://rp.local.conformance.test/",
"commands": [
["wait", "id", "submission_complete", 30]
]
}
]
}
]
}
4 changes: 4 additions & 0 deletions conformance-tests/trigger-client.py
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,10 @@ def trigger_rp():
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)
elif "logout_rejected" in trigger_resp.text:
# Negative RP-Initiated Logout test modules: the RP
# completed the flow by rejecting the logout callback.
print("SUCCESS: logout_rejected 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
Expand Down
5 changes: 5 additions & 0 deletions docker/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,11 @@ services:
- CLIENT_SECRET=oidc-client-php-test-secret
- REDIRECT_URI=https://rp.local.conformance.test/callback
- SCOPE=openid profile email
# 'none' runs plain login flows; 'rp_initiated' continues each login
# with an RP-Initiated Logout flow (for the RP logout conformance plan).
- LOGOUT_FLOW=${LOGOUT_FLOW:-none}
- POST_LOGOUT_REDIRECT_URI=https://rp.local.conformance.test/logout-callback
- BACKCHANNEL_LOGOUT_URI=https://rp.local.conformance.test/backchannel-logout
extra_hosts:
- "localhost.emobix.co.uk:host-gateway"

Expand Down
64 changes: 60 additions & 4 deletions docker/rp-app/index.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
require_once __DIR__ . '/vendor/autoload.php';

use Cicnavi\Oidc\DynamicallyRegisteredClient;
use Cicnavi\Oidc\Exceptions\OidcClientException;
use Cicnavi\Oidc\PreRegisteredClient;
use Cicnavi\Oidc\CodeBooks\AuthorizationRequestMethodEnum;
use GuzzleHttp\Client as GuzzleClient;
Expand All @@ -19,10 +20,33 @@
$clientRegistration = getenv('CLIENT_REGISTRATION') ?: 'static_client';
$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';
$rpBaseUri = getenv('RP_BASE_URI') ?: 'https://rp.local.conformance.test';
$redirectUri = getenv('REDIRECT_URI') ?: $rpBaseUri . '/callback';
$scope = getenv('SCOPE') ?: 'openid';
// 'none' runs plain login flows; 'rp_initiated' continues each login with an
// RP-Initiated Logout flow (for the RP-Initiated Logout conformance plan).
$logoutFlow = getenv('LOGOUT_FLOW') ?: 'none';
$postLogoutRedirectUri = getenv('POST_LOGOUT_REDIRECT_URI') ?: $rpBaseUri . '/logout-callback';
$backchannelLogoutUri = getenv('BACKCHANNEL_LOGOUT_URI') ?: $rpBaseUri . '/backchannel-logout';

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

if ($path === '/backchannel-logout') {
// Back-Channel Logout endpoint stub. The RP-Initiated Logout test
// modules require the client to have a backchannel_logout_uri or
// frontchannel_logout_uri registered, and the suite POSTs a logout
// token here while handling the end_session request. Proper logout
// token validation is a separate library feature (Back-Channel
// Logout support) - until it lands, only acknowledge the request.
// 'Cache-Control: no-store' is required per OIDC Back-Channel Logout
// 2.8 (the suite warns when missing).
header('Cache-Control: no-store');
http_response_code(200);
echo 'OK';
exit;
}

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

Expand All @@ -33,7 +57,12 @@
scope: $scope,
clientName: 'oidc-client-php',
httpClient: $httpClient,
defaultAuthorizationRequestMethod: AuthorizationRequestMethodEnum::Query
defaultAuthorizationRequestMethod: AuthorizationRequestMethodEnum::Query,
postLogoutRedirectUris: $logoutFlow === 'rp_initiated' ? [$postLogoutRedirectUri] : [],
// See the /backchannel-logout endpoint stub above.
additionalClientMetadata: $logoutFlow === 'rp_initiated'
? ['backchannel_logout_uri' => $backchannelLogoutUri]
: [],
);
} else {
$client = new PreRegisteredClient(
Expand All @@ -47,17 +76,44 @@
);
}

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

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

if ($logoutFlow === 'rp_initiated') {
// Continue with RP-Initiated Logout in a separate request, so the
// persisted login data (ID token for 'id_token_hint') is read
// from the session store the way a real application would.
header('Location: ' . $rpBaseUri . '/logout', true, 302);
exit;
}

// 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>';
} elseif ($path === '/logout') {
// Redirects the user agent to the OP's end_session_endpoint with
// id_token_hint, client_id, post_logout_redirect_uri and state.
$client->logout(postLogoutRedirectUri: $postLogoutRedirectUri);
} elseif ($path === '/logout-callback') {
try {
$client->validateLogoutCallback();

echo '<html><head><title>OIDC RP Logout Completion</title></head><body>';
echo '<div id="submission_complete">RP-Initiated Logout Successful!</div>';
echo '<p>Login data cleared: ' . ($client->getLoginData() === null ? 'yes' : 'NO') . '</p>';
echo '</body></html>';
} catch (OidcClientException $exception) {
// Negative test modules (state omitted or changed by the OP) end
// up here: the RP must not treat the logout as confirmed.
http_response_code(400);
echo '<html><head><title>OIDC RP Logout Rejected</title></head><body>';
echo '<div id="logout_rejected">Post logout callback rejected, logout is NOT confirmed: '
. htmlspecialchars($exception->getMessage()) . '</div>';
echo '</body></html>';
}
} else {
if ($client instanceof DynamicallyRegisteredClient) {
// Each conformance test module is a fresh OP instance served on the
Expand Down
132 changes: 66 additions & 66 deletions docs/1-Index.md
Original file line number Diff line number Diff line change
@@ -1,66 +1,66 @@
# OIDC Client PHP
## Prerequisites
PHP environment:
* Check `composer.json` for environment requirements.
* ODIC client uses PHP session by default to handle `state`, `nonce` and
`code_verifier` parameters storage and validation. If the session is not
already started, the OIDC client will try to start it using session config
from `php.ini`.
OpenID Provider must support:
* Authorization Code Flow
* OIDC Discovery URL (`.well-known` URL with OP metadata)
* JWKS URI providing JWK key(s)
## Installation
OIDC Client is available as a Composer package. In your project you can run:
```shell script
composer require cicnavi/oidc-client-php
```
## Client Usage
There are three ways to instantiate an OIDC client:
* Pre-registered Client (`Cicnavi\Oidc\PreRegisteredClient`) - can be used if
the client is already registered with the OpenID Provider.
* Federated Client (`Cicnavi\Oidc\FederatedClient`) - can be used in federated
environments (as per OpenID Federation specification). This client type
currently supports Automatic Client Registration flow using Request Object
passed by value.
* Dynamically Registered Client (`Cicnavi\Oidc\DynamicallyRegisteredClient`) -
can be used if the OpenID Provider supports OpenID Connect Dynamic Client
Registration 1.0. The client registers itself with the OpenID Provider and
uses the issued client credentials.
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)
* [Dynamically Registered Client](4-Dynamically-Registered-Client.md)
* [Conformance Testing](5-Conformance-Testing.md)
## Note on SameSite Cookie Attribute
[SameSite Cookie](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie/SameSite)
attribute plays an important role in Single Sign-On (SSO) environments
because it determines how cookies are delivered in third party contexts.
During OIDC authorization code flow (the authentication flow this OIDC client
uses), a series of HTTP redirects between RP and OP is performed.
By default, the authorization code will be delivered to the RP using HTTP
Redirect, meaning that the User Agent will do a GET request to the RP callback.
This means that the SameSite Cookie attribute can be set to `Lax` or `None`,
but not `Strict` (if the value is `None`, the attribute `Secure` must also
be set).
## Run tests
All tests are available as Composer scripts, so you can run them like this:
```bash
$ composer run-script test
```
# OIDC Client PHP

## Prerequisites

PHP environment:
* Check `composer.json` for environment requirements.
* ODIC client uses PHP session by default to handle `state`, `nonce` and
`code_verifier` parameters storage and validation. If the session is not
already started, the OIDC client will try to start it using session config
from `php.ini`.

OpenID Provider must support:
* Authorization Code Flow
* OIDC Discovery URL (`.well-known` URL with OP metadata)
* JWKS URI providing JWK key(s)

## Installation

OIDC Client is available as a Composer package. In your project you can run:

```shell script
composer require cicnavi/oidc-client-php
```

## Client Usage

There are three ways to instantiate an OIDC client:
* Pre-registered Client (`Cicnavi\Oidc\PreRegisteredClient`) - can be used if
the client is already registered with the OpenID Provider.
* Federated Client (`Cicnavi\Oidc\FederatedClient`) - can be used in federated
environments (as per OpenID Federation specification). This client type
currently supports Automatic Client Registration flow using Request Object
passed by value.
* Dynamically Registered Client (`Cicnavi\Oidc\DynamicallyRegisteredClient`) -
can be used if the OpenID Provider supports OpenID Connect Dynamic Client
Registration 1.0. The client registers itself with the OpenID Provider and
uses the issued client credentials.

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)
* [Dynamically Registered Client](4-Dynamically-Registered-Client.md)
* [Conformance Testing](5-Conformance-Testing.md)


## Note on SameSite Cookie Attribute

[SameSite Cookie](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie/SameSite)
attribute plays an important role in Single Sign-On (SSO) environments
because it determines how cookies are delivered in third party contexts.
During OIDC authorization code flow (the authentication flow this OIDC client
uses), a series of HTTP redirects between RP and OP is performed.

By default, the authorization code will be delivered to the RP using HTTP
Redirect, meaning that the User Agent will do a GET request to the RP callback.
This means that the SameSite Cookie attribute can be set to `Lax` or `None`,
but not `Strict` (if the value is `None`, the attribute `Secure` must also
be set).

## Run tests

All tests are available as Composer scripts, so you can run them like this:

```bash
$ composer run-script test
```
Loading
Loading