From 51889afb9ba8942f26d1c92b1e59fb6bea16ba05 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Ivan=C4=8Di=C4=87?= Date: Wed, 3 Jun 2026 17:30:04 +0200 Subject: [PATCH 1/3] Start with Basic conformance tests --- .github/workflows/build.yml | 53 +++++++++- conformance-tests/basic-skips.json | 7 ++ conformance-tests/basic-warnings.json | 1 + conformance-tests/conformance-basic-ci.json | 24 +++++ conformance-tests/trigger-client.py | 110 ++++++++++++++++++++ docker/Dockerfile | 39 +++++++ docker/docker-compose.yml | 21 ++++ docker/rp-app/index.php | 59 +++++++++++ docker/rp-ssl.conf | 18 ++++ docs/1-Index.md | 1 + docs/4-Conformance-Testing.md | 72 +++++++++++++ src/FederatedClient.php | 3 +- src/PreRegisteredClient.php | 7 +- src/Protocol/RequestDataHandler.php | 68 ++++++++++++ tests/Oidc/PreRegisteredClientTest.php | 3 +- 15 files changed, 482 insertions(+), 4 deletions(-) create mode 100644 conformance-tests/basic-skips.json create mode 100644 conformance-tests/basic-warnings.json create mode 100644 conformance-tests/conformance-basic-ci.json create mode 100644 conformance-tests/trigger-client.py create mode 100644 docker/Dockerfile create mode 100644 docker/docker-compose.yml create mode 100644 docker/rp-app/index.php create mode 100644 docker/rp-ssl.conf create mode 100644 docs/4-Conformance-Testing.md diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 946e48c..070d30f 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -22,4 +22,55 @@ jobs: - name: Run test suite run: composer run-script pre-commit - name: Show PHP version - run: php -v \ No newline at end of file + 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 --project-directory . 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 \ No newline at end of file diff --git a/conformance-tests/basic-skips.json b/conformance-tests/basic-skips.json new file mode 100644 index 0000000..bf94f1b --- /dev/null +++ b/conformance-tests/basic-skips.json @@ -0,0 +1,7 @@ +[ + { + "test-name": "oidcc-client-test-idtoken-sig-none", + "variant": "*", + "configuration-filename": "*" + } +] diff --git a/conformance-tests/basic-warnings.json b/conformance-tests/basic-warnings.json new file mode 100644 index 0000000..fe51488 --- /dev/null +++ b/conformance-tests/basic-warnings.json @@ -0,0 +1 @@ +[] diff --git a/conformance-tests/conformance-basic-ci.json b/conformance-tests/conformance-basic-ci.json new file mode 100644 index 0000000..9de29f7 --- /dev/null +++ b/conformance-tests/conformance-basic-ci.json @@ -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] + ] + } + ] + } + ] +} diff --git a/conformance-tests/trigger-client.py b/conformance-tests/trigger-client.py new file mode 100644 index 0000000..9613654 --- /dev/null +++ b/conformance-tests/trigger-client.py @@ -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) diff --git a/docker/Dockerfile b/docker/Dockerfile new file mode 100644 index 0000000..2e537ff --- /dev/null +++ b/docker/Dockerfile @@ -0,0 +1,39 @@ +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/ + +# Install PHP dependencies +RUN composer install --no-dev --prefer-dist --optimize-autoloader + +# 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 diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml new file mode 100644 index 0000000..427f517 --- /dev/null +++ b/docker/docker-compose.yml @@ -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 diff --git a/docker/rp-app/index.php b/docker/rp-app/index.php new file mode 100644 index 0000000..6079871 --- /dev/null +++ b/docker/rp-app/index.php @@ -0,0 +1,59 @@ + 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 'OIDC RP Test Completion'; + echo '
OIDC Flow Successful!
'; + echo '

User Data

' . htmlspecialchars(json_encode($userData, JSON_PRETTY_PRINT)) . '
'; + echo ''; + } else { + // Initiate OIDC authorization flow + $client->authorize(); + } +} catch (\Throwable $e) { + http_response_code(500); + echo 'OIDC RP Test Error'; + echo '

Error

'; + echo '
' . htmlspecialchars($e->getMessage()) . "\n" . htmlspecialchars($e->getTraceAsString()) . '
'; + echo ''; + exit; +} diff --git a/docker/rp-ssl.conf b/docker/rp-ssl.conf new file mode 100644 index 0000000..e340a09 --- /dev/null +++ b/docker/rp-ssl.conf @@ -0,0 +1,18 @@ + + 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 + + + AllowOverride All + Require all granted + FallbackResource /index.php + + + # Log configuration + ErrorLog ${APACHE_LOG_DIR}/error.log + CustomLog ${APACHE_LOG_DIR}/access.log combined + diff --git a/docs/1-Index.md b/docs/1-Index.md index acd2408..fefc4e4 100644 --- a/docs/1-Index.md +++ b/docs/1-Index.md @@ -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 diff --git a/docs/4-Conformance-Testing.md b/docs/4-Conformance-Testing.md new file mode 100644 index 0000000..9a368fc --- /dev/null +++ b/docs/4-Conformance-Testing.md @@ -0,0 +1,72 @@ +# OpenID Connect Relying Party Conformance Testing + +The `oidc-client-php` library has been fully tested and verified against the official **OpenID Connect Relying Party (RP) Conformance Suite**. + +Specifically, currently we run the following OpenID Conformance Tests: +* **Basic RP profile** (`oidcc-client-basic-certification-test-plan` plan using static client registration and plain HTTP request authorization). + +--- + +## How to Run Conformance Tests Locally + +### Prerequisites + +- **Docker and Docker Compose** installed. +- **Python 3** with `httpx` package installed: + ```bash + pip install httpx + ``` + +--- + +### Step 1: Set Up the Conformance Suite + +1. Clone the OpenID Conformance Suite repository: + ```bash + git clone --depth 1 --single-branch --branch release-v5.1.40 https://gitlab.com/openid/conformance-suite.git + ``` +2. Build the conformance suite (requires Docker): + ```bash + cd conformance-suite + # Set localhost mapping for internal routing + 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 + ``` +3. Start the conformance suite: + ```bash + docker compose -f docker-compose-dev.yml up -d + ``` +4. Verify the suite is healthy (it should return a valid JSON list of plans/available runner endpoints): + ```bash + curl -sk https://localhost.emobix.co.uk:8443/api/runner/available + ``` + +--- + +### Step 2: Start the RP Test Application + +1. Go back to your local `oidc-client-php` directory. +2. Build and start the Relying Party test application Docker container: + ```bash + docker compose -f docker/docker-compose.yml up --build -d + ``` + +--- + +### Step 3: Run the Conformance Tests + +1. Start the OIDC trigger client daemon in the background (or in a separate terminal window): + ```bash + python3 conformance-tests/trigger-client.py + ``` +2. Run the test plan runner script: + ```bash + python3 /path/to/conformance-suite/scripts/run-test-plan.py \ + --expected-failures-file conformance-tests/basic-warnings.json \ + --expected-skips-file conformance-tests/basic-skips.json \ + "oidcc-client-basic-certification-test-plan[client_registration=static_client][request_type=plain_http_request]" \ + conformance-tests/conformance-basic-ci.json + ``` + +All test modules should complete and pass cleanly. diff --git a/src/FederatedClient.php b/src/FederatedClient.php index a4f2348..d788549 100644 --- a/src/FederatedClient.php +++ b/src/FederatedClient.php @@ -870,7 +870,8 @@ public function getUserData(?ServerRequestInterface $request = null): array clientAssertion: $clientAssertion->getToken(), usePkce: $this->usePkce, useNonce: $this->useNonce, - fetchUserinfoClaims: $this->fetchUserinfoClaims + fetchUserinfoClaims: $this->fetchUserinfoClaims, + expectedIssuer: $opEntityId, ); } diff --git a/src/PreRegisteredClient.php b/src/PreRegisteredClient.php index 7bc8ab2..4c6b4ff 100644 --- a/src/PreRegisteredClient.php +++ b/src/PreRegisteredClient.php @@ -288,6 +288,10 @@ public function getUserData(?ServerRequestInterface $request = null): array $opUserinfoEndpoint = $this->metadata->get(ClaimsEnum::UserinfoEndpoint->value); $opUserinfoEndpoint = is_string($opUserinfoEndpoint) ? $opUserinfoEndpoint : null; + $expectedIssuer = is_string($expectedIssuer = $this->metadata->get(ClaimsEnum::Issuer->value)) ? + $expectedIssuer : + null; + return $this->requestDataHandler->getUserData( clientAuthenticationMethod: ClientAuthenticationMethodsEnum::ClientSecretBasic, @@ -301,7 +305,8 @@ public function getUserData(?ServerRequestInterface $request = null): array clientAssertion: null, usePkce: $this->usePkce, useNonce: $this->useNonce, - fetchUserinfoClaims: $this->fetchUserinfoClaims + fetchUserinfoClaims: $this->fetchUserinfoClaims, + expectedIssuer: $expectedIssuer, ); } diff --git a/src/Protocol/RequestDataHandler.php b/src/Protocol/RequestDataHandler.php index 2084aa4..b0dc2b1 100644 --- a/src/Protocol/RequestDataHandler.php +++ b/src/Protocol/RequestDataHandler.php @@ -119,6 +119,7 @@ public function getUserData( bool $usePkce = true, bool $useNonce = true, bool $fetchUserinfoClaims = true, + ?string $expectedIssuer = null, ): array { $tokenData = $this->requestTokenData( @@ -145,6 +146,8 @@ public function getUserData( userinfoEndpoint: $opUserinfoEndpoint, useNonce: $useNonce, fetchUserinfoClaims: $fetchUserinfoClaims, + expectedIssuer: $expectedIssuer, + expectedClientId: $clientId, ); } @@ -403,6 +406,8 @@ public function getClaims( ?string $userinfoEndpoint = null, bool $useNonce = true, bool $fetchUserinfoClaims = true, + ?string $expectedIssuer = null, + ?string $expectedClientId = null, ): array { $idTokenClaims = []; $userInfoClaims = []; @@ -413,6 +418,9 @@ public function getClaims( idToken: $idToken, jwksUri: $jwksUri, useNonce: $useNonce, + refreshCache: false, + expectedIssuer: $expectedIssuer, + expectedClientId: $expectedClientId, ); } @@ -443,6 +451,8 @@ public function getDataFromIdToken( string $jwksUri, bool $useNonce = true, bool $refreshCache = false, + ?string $expectedIssuer = null, + ?string $expectedClientId = null, ): array { $jwks = $this->getJwksUriContent($jwksUri, $refreshCache); @@ -474,9 +484,67 @@ public function getDataFromIdToken( jwksUri: $jwksUri, useNonce: $useNonce, refreshCache: true, + expectedIssuer: $expectedIssuer, + expectedClientId: $expectedClientId, ); } + // Validate Issuer (iss) + if ($expectedIssuer !== null) { + $iss = $idTokenJws->getIssuer(); + if ($iss !== $expectedIssuer) { + $error = sprintf('Issuer claim "%s" does not match expected issuer "%s".', $iss, $expectedIssuer); + $this->logger?->error($error); + throw new OidcClientException($error); + } + } + + // Validate Audience (aud) and Authorized Party (azp) + if ($expectedClientId !== null) { + $aud = $idTokenJws->getAudience(); + if ($aud !== [] && !in_array($expectedClientId, $aud, true)) { + $error = sprintf('Audience claim does not contain expected client ID "%s".', $expectedClientId); + $this->logger?->error($error); + throw new OidcClientException($error); + } + + if (count($aud) > 1) { + $azp = $idTokenJws->getAuthorizedParty(); + if ($azp === null) { + $error = 'Authorized party claim (azp) is missing but multiple audiences are present.'; + $this->logger?->error($error); + throw new OidcClientException($error); + } + + if ($azp !== $expectedClientId) { + $error = sprintf( + 'Authorized party claim "%s" does not match expected client ID "%s".', + $azp, + $expectedClientId, + ); + $this->logger?->error($error); + throw new OidcClientException($error); + } + } + } + + // Validate Expiration Time (exp) + $exp = $idTokenJws->getExpirationTime(); + if ($exp > 0 && time() > $exp) { + $error = 'ID Token has expired.'; + $this->logger?->error($error); + throw new OidcClientException($error); + } + + // Validate Issued At (iat) + $iat = $idTokenJws->getIssuedAt(); + // Allow a small clock skew (e.g. 5 minutes or 300 seconds) + if ($iat > 0 && time() < ($iat - 300)) { + $error = 'ID Token was issued in the future.'; + $this->logger?->error($error); + throw new OidcClientException($error); + } + if ($useNonce) { if (($nonce = $idTokenJws->getNonce()) === null) { $this->logger?->error('ID token nonce not found.'); diff --git a/tests/Oidc/PreRegisteredClientTest.php b/tests/Oidc/PreRegisteredClientTest.php index 8545a06..d6efd42 100644 --- a/tests/Oidc/PreRegisteredClientTest.php +++ b/tests/Oidc/PreRegisteredClientTest.php @@ -275,10 +275,11 @@ public function testGetUserDataSuccess(): void \SimpleSAML\OpenID\Codebooks\ParamsEnum::Code->value => 'auth-code-123', ]); - $this->metadataMock->expects($this->exactly(3))->method('get')->willReturnMap([ + $this->metadataMock->expects($this->exactly(4))->method('get')->willReturnMap([ [\SimpleSAML\OpenID\Codebooks\ClaimsEnum::JwksUri->value, 'https://op.example.org/jwks'], [\SimpleSAML\OpenID\Codebooks\ClaimsEnum::TokenEndpoint->value, 'https://op.example.org/token'], [\SimpleSAML\OpenID\Codebooks\ClaimsEnum::UserinfoEndpoint->value, 'https://op.example.org/userinfo'], + [\SimpleSAML\OpenID\Codebooks\ClaimsEnum::Issuer->value, 'https://op.example.org'], ]); $expected = ['sub' => 'user-1']; From e37122b7774e5fcd208dee9430ecb60bc0966a3f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Ivan=C4=8Di=C4=87?= Date: Wed, 3 Jun 2026 17:42:00 +0200 Subject: [PATCH 2/3] Remove projecd-directory param --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 070d30f..1f2e98e 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -54,7 +54,7 @@ jobs: - name: Start RP App Docker working-directory: ./main run: | - docker compose -f docker/docker-compose.yml --project-directory . up -d --build + docker compose -f docker/docker-compose.yml up -d --build sleep 5 - name: Start trigger-client daemon run: | From 392b202416616f191b6956828d28fedec5ab7cb0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Ivan=C4=8Di=C4=87?= Date: Wed, 3 Jun 2026 18:01:09 +0200 Subject: [PATCH 3/3] Fix Dockerfile --- docker/Dockerfile | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/docker/Dockerfile b/docker/Dockerfile index 2e537ff..4f904a3 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -29,8 +29,11 @@ COPY --from=composer:latest /usr/bin/composer /usr/bin/composer 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 +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