From 2b994f58989cf8603c20fd8b36289a87e896a69a Mon Sep 17 00:00:00 2001 From: Benjamin Renard Date: Wed, 24 Jul 2024 18:13:46 +0200 Subject: [PATCH] Initial version --- .forgejo/workflows/tests.yaml | 14 ++ .gitignore | 2 + .pre-commit-config.yaml | 33 +++ README.md | 39 ++++ composer.json | 8 + phpstan.neon | 4 + public_html/index.php | 378 ++++++++++++++++++++++++++++++++++ 7 files changed, 478 insertions(+) create mode 100644 .forgejo/workflows/tests.yaml create mode 100644 .gitignore create mode 100644 .pre-commit-config.yaml create mode 100644 README.md create mode 100644 composer.json create mode 100644 phpstan.neon create mode 100644 public_html/index.php diff --git a/.forgejo/workflows/tests.yaml b/.forgejo/workflows/tests.yaml new file mode 100644 index 0000000..e06551d --- /dev/null +++ b/.forgejo/workflows/tests.yaml @@ -0,0 +1,14 @@ +--- +name: Run tests +on: [push] +jobs: + tests: + runs-on: docker + container: + image: docker.io/brenard/python-pre-commit:latest + options: "--workdir /src" + steps: + - name: Check out repository code + uses: actions/checkout@v4 + - name: Run pre-commit + run: pre-commit run --all-files diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..de4a392 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/vendor +/composer.lock diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..554249e --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,33 @@ +# Pre-commit hooks to run tests and ensure code is cleaned. +# See https://pre-commit.com for more information +--- +repos: + - repo: https://github.com/codespell-project/codespell + rev: v2.2.2 + hooks: + - id: codespell + exclude: static/lib/|locales/.*\.js$|\.pot$ + args: ["--write-changes"] + exclude_types: [csv, json] + - repo: https://github.com/adrienverge/yamllint + rev: v1.32.0 + hooks: + - id: yamllint + ignore: .github/ + - repo: https://github.com/pre-commit/mirrors-prettier + rev: v2.7.1 + hooks: + - id: prettier + - repo: https://github.com/digitalpulp/pre-commit-php.git + rev: 1.4.0 + hooks: + - id: php-stan + files: ^(?!example/).*\.(php)$ + args: ["--configuration=phpstan.neon"] + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.4.0 + hooks: + - id: check-executables-have-shebangs + stages: [manual] + - id: check-json + exclude: (.vscode|.devcontainer) diff --git a/README.md b/README.md new file mode 100644 index 0000000..b2e72d6 --- /dev/null +++ b/README.md @@ -0,0 +1,39 @@ +# Test OIDC Client + +This application acting as OIDC Client. + +Features : + +- Login/Logout on OIDC server +- Local logout +- Show authenticated user's information (token ID, claims and attributes) + +## Requirements + +- PHP 7.0 or greater +- `composer` to install dependencies (from `composer` Debian packages for instance, + see https://getcomposer.org/download/ otherwise) +- PHP `curl` and `json` extensions (from `php-curl` and `php-json` Debian packages for instance) +- Apache PHP support (using _mod_php_ or _PHP Fpm_, install _libapache2-mod-php_ Debian packages for instance) + +## Installation + +- Install the application and its requirements + +``` +apt install -y libapache2-mod-php php-curl php-json git composer +mkdir -p /var/www/connection/ +git clone https://gogs.zionetrix.net/bn8/test-oidc-client.git /var/www/connection/test-oidc-client +cd /var/www/connection/test-oidc-client +composer install +``` + +- Configure access to the application, for instance, in an existing Apache _VirtualHost_ definition by adding something like: + +``` +Alias /test-oidc /var/www/connection/test-oidc-client/public_html +ProxyPass /test-oidc ! + + Require all granted + +``` diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..2f12860 --- /dev/null +++ b/composer.json @@ -0,0 +1,8 @@ +{ + "require": { + "jumbojett/openid-connect-php": "^1.0" + }, + "require-dev": { + "phpstan/phpstan": "^1.11" + } +} diff --git a/phpstan.neon b/phpstan.neon new file mode 100644 index 0000000..ed84782 --- /dev/null +++ b/phpstan.neon @@ -0,0 +1,4 @@ +parameters: + level: 5 + paths: + - public_html diff --git a/public_html/index.php b/public_html/index.php new file mode 100644 index 0000000..380e660 --- /dev/null +++ b/public_html/index.php @@ -0,0 +1,378 @@ + [ + // OIDC server root URL + "url" => "https://connection.example.com", + + // Client ID (as provided by your IDP) + "client_id" => "27d65748-0aaa-42d9-90dc-353d94f2840a", + + // Client secret (as provided by your IDP) + "client_secret" => "5d86b271-a3b8-4d75-b569-a86d48b75658", + + // Requested OIDC scopes + "scopes" => ["profile", "email"], + + // Expected attributes (optional, retrieve all proposed attributes otherwise) + /* + "expected_attributes" => [ + "preferred_username", + "given_name", + "family_name", + "email", + ], + */ + ], +]; + +// FQDN of OIDC server +$default_oidc_server = key($oidc_servers); + +/* + + ************************************ + * Main * + ************************************ + +*/ + +session_start(); + +$_SESSION["warnings"] = isset($_SESSION["warnings"]) && is_array($_SESSION["warnings"])?$_SESSION["warnings"]:[]; + +if (isset($_REQUEST['server']) && !isset($oidc_servers[$_REQUEST['server']])) { + $_SESSION["warnings"][] = "Invalid OIDC server choiced"; + unset($_REQUEST['server']); +} + +if (isset($_REQUEST['server'])) { + $oidc_server = $_REQUEST['server']; + if ($_SESSION['oidc_server'] != $oidc_server) { + $_SESSION = []; // reset session on changing OIDC server + } +} +elseif (isset($_SESSION['oidc_server'])) { + $oidc_server = $_SESSION['oidc_server']; +} +else { + $oidc_server = $default_oidc_server; + $_SESSION = []; // reset session +} +$_SESSION['oidc_server'] = $oidc_server; + +function reset_session() { + global $oidc_server; + $_SESSION = []; + if (isset($oidc_server) && $oidc_server) + $_SESSION['oidc_server'] = $oidc_server; +} + +$_show_oidc_client_config = false; +function show_oidc_client_config() { + global $_show_oidc_client_config, $oidc_client_config; + if ($_show_oidc_client_config) return true; + $_show_oidc_client_config = true; + echo "

OIDC Client configuration

"; +} + +function show_warnings() { + if (!empty($_SESSION["warnings"])) { + echo "

Warnings message

"; + } + $_SESSION["warnings"] = []; +} + +$_SESSION["messages"] = isset($_SESSION["messages"]) && is_array($_SESSION["messages"])?$_SESSION["messages"]:[]; +function show_messages() { + if (!empty($_SESSION["messages"])) { + echo "
"; + if (count($_SESSION["messages"]) == 1) { + echo $_SESSION["messages"][0]; + } + else { + echo ""; + } + echo "
"; + } + $_SESSION["messages"] = []; +} + +function vardump($value) { + ob_start(); + var_dump($value); + $value = ob_get_contents(); + ob_end_clean(); + return $value; +} + +function redirect($url=null) { + global $public_app_url; + $url = $url?$url:$public_app_url; + header("Location: $url"); + exit(); +} + +function show_user_infos() { + echo "

Token ID

"; + echo "
".vardump($_SESSION['id_token'])."
"; + + echo "

Verified claims

"; + echo "
".vardump($_SESSION['verified_claims'])."
"; + + echo "

Attributes

"; + echo ""; +} + +?> + + + Test OIDC + + + +

Test OIDC Application

+ + + +

OIDC server selection

+
+ : + + +
+

Menu

+ + +

OIDC Client Initialization ...

+ $public_app_url, + 'OIDC Hostname' => $oidc_server, + 'IDP URL' => $oidc_servers[$oidc_server]['url'], + 'Client ID' => ( + substr($oidc_servers[$oidc_server]['client_id'], 0, 4)."...". + substr($oidc_servers[$oidc_server]['client_id'], -2) + ), + 'Client secret' => ( + substr($oidc_servers[$oidc_server]['client_secret'], 0, 4)."...". + substr($oidc_servers[$oidc_server]['client_secret'], -2) + ), +]; + +$oidc = new OpenIDConnectClient( + $oidc_servers[$oidc_server]['url'], + $oidc_servers[$oidc_server]['client_id'], + $oidc_servers[$oidc_server]['client_secret'] +); + +$client_redirect_url = $public_app_url."/?do=login"; +$oidc_client_config["Client redirect URL"] = $client_redirect_url; +$oidc->setRedirectURL($client_redirect_url); + +$client_logout_redirect_url = "$public_app_url/?do=logout_callback"; +$oidc_client_config["Client logout redirect URL"] = $client_logout_redirect_url; + +$oidc_client_config["Scopes"] = implode(", ", $oidc_servers[$oidc_server]['scopes']); +$oidc->addScope($oidc_servers[$oidc_server]['scopes']); + +$oidc_client_config['Expected attributes'] = ( + isset($oidc_servers[$oidc_server]['expected_attributes']) // @phpstan-ignore-line + && is_array($oidc_servers[$oidc_server]['expected_attributes']) + && ! empty($oidc_servers[$oidc_server]['expected_attributes'])? + implode(", ", $oidc_servers[$oidc_server]['expected_attributes']): + "none (retrieve all provided attributes)" +); + +echo "

Client successfully initialized

"; + +show_oidc_client_config(); +show_warnings(); + +?> + +

Action

+

State before running action

+ +

Running action...

+authenticate(); + } + catch(OpenIDConnectClientException $e) { + $_SESSION["warnings"][] = "Fail to authenticate: ".$e->getMessage(); + redirect(); + } + $_SESSION["messages"][] = "Successfully authenticated"; + $_SESSION['id_token'] = $oidc->getIdToken(); + $_SESSION['verified_claims'] = $oidc->getVerifiedClaims(); + + $_SESSION['attributes'] = []; + if ( + isset($oidc_servers[$oidc_server]['expected_attributes']) // @phpstan-ignore-line + && is_array($oidc_servers[$oidc_server]['expected_attributes']) + && !empty($oidc_servers[$oidc_server]['expected_attributes']) + ) { + foreach($oidc_servers[$oidc_server]['expected_attributes'] as $attr) { + try { + $_SESSION['attributes'][$attr] = $oidc->requestUserInfo($attr); + } + catch(OpenIDConnectClientException $e) { + $_SESSION["warnings"][] = "Fail to retrieve attribute '$attr': ".$e->getMessage(); + } + } + } + else { + try { + $_SESSION['attributes'] = get_object_vars($oidc->requestUserInfo()); + } + catch(OpenIDConnectClientException $e) { + $_SESSION["warnings"][] = "Fail to retrieve attributes: ".$e->getMessage(); + } + } + redirect(); + break; + case 'logout': + if (isset($_SESSION['id_token'])) { + $_SESSION["logout_expected"] = true; + $oidc->signOut($_SESSION['id_token'], $client_logout_redirect_url); + } + else { + $_SESSION["warnings"] = "Not yet authenticated, can't logout!"; + redirect(); + } + break; + case "logout_callback": + if (isset($_SESSION["logout_expected"]) && $_SESSION["logout_expected"]) { + $_SESSION = ["oidc_server" => $oidc_server]; + $_SESSION["messages"][] = "Successfully logout from OIDC server and client application."; + unset($_SESSION["logout_expected"]); + } + else { + $_SESSION["warnings"] = "Unexpected logout callback URL call"; + } + redirect(); + break; + case 'local_logout': + $_SESSION = ["oidc_server" => $oidc_server]; + $_SESSION["messages"][] = "Successfully logout from client application only."; + redirect(); + break; + default: + $_SESSION["warningsverified_claims"] = "Incorrect requested action"; + redirect(); + } +} +else { + echo "Nothing to do"; +} + +if (isset($_SESSION['id_token'])) { + echo "

Authenticated user information

"; + show_user_infos(); +} + +?> + + +