From 480731d97b78198eb37235a4589bf9ca08b03f3f Mon Sep 17 00:00:00 2001 From: Mikkel Ricky Date: Mon, 18 Aug 2025 15:05:02 +0200 Subject: [PATCH 01/15] 5124: Experiments # Conflicts: # composer.json # composer.lock --- .../modules/os2loop_login_hack/.gitignore | 2 + .../modules/os2loop_login_hack/composer.json | 15 ++ .../os2loop_login_hack.info.yml | 7 + .../os2loop_login_hack.routing.yml | 17 ++ .../os2loop_login_hack.services.yml | 4 + .../Controller/Os2loopLoginHackController.php | 167 ++++++++++++++++++ 6 files changed, 212 insertions(+) create mode 100644 web/profiles/custom/os2loop/modules/os2loop_login_hack/.gitignore create mode 100644 web/profiles/custom/os2loop/modules/os2loop_login_hack/composer.json create mode 100644 web/profiles/custom/os2loop/modules/os2loop_login_hack/os2loop_login_hack.info.yml create mode 100644 web/profiles/custom/os2loop/modules/os2loop_login_hack/os2loop_login_hack.routing.yml create mode 100644 web/profiles/custom/os2loop/modules/os2loop_login_hack/os2loop_login_hack.services.yml create mode 100644 web/profiles/custom/os2loop/modules/os2loop_login_hack/src/Controller/Os2loopLoginHackController.php diff --git a/web/profiles/custom/os2loop/modules/os2loop_login_hack/.gitignore b/web/profiles/custom/os2loop/modules/os2loop_login_hack/.gitignore new file mode 100644 index 000000000..d8a7996ab --- /dev/null +++ b/web/profiles/custom/os2loop/modules/os2loop_login_hack/.gitignore @@ -0,0 +1,2 @@ +composer.lock +vendor/ diff --git a/web/profiles/custom/os2loop/modules/os2loop_login_hack/composer.json b/web/profiles/custom/os2loop/modules/os2loop_login_hack/composer.json new file mode 100644 index 000000000..dc3ef3cc9 --- /dev/null +++ b/web/profiles/custom/os2loop/modules/os2loop_login_hack/composer.json @@ -0,0 +1,15 @@ +{ + "name": "os2loop/os2loop_login_hack", + "description": "drupal/os2loop_login_hack", + "license": "GPL-2.0+", + "type": "os2loop-custom-module", + "authors": [ + { + "name": "Mikkel Ricky", + "email": "rimi@aarhus.dk" + } + ], + "require": { + "firebase/php-jwt": "^6.11" + } +} diff --git a/web/profiles/custom/os2loop/modules/os2loop_login_hack/os2loop_login_hack.info.yml b/web/profiles/custom/os2loop/modules/os2loop_login_hack/os2loop_login_hack.info.yml new file mode 100644 index 000000000..8d23c2764 --- /dev/null +++ b/web/profiles/custom/os2loop/modules/os2loop_login_hack/os2loop_login_hack.info.yml @@ -0,0 +1,7 @@ +name: 'os2loop_login_hack' +type: module +description: 'os2loop_login_hack' +package: Custom +core_version_requirement: ^10 || ^11 +dependencies: + - drupal:user diff --git a/web/profiles/custom/os2loop/modules/os2loop_login_hack/os2loop_login_hack.routing.yml b/web/profiles/custom/os2loop/modules/os2loop_login_hack/os2loop_login_hack.routing.yml new file mode 100644 index 000000000..ce5be46b5 --- /dev/null +++ b/web/profiles/custom/os2loop/modules/os2loop_login_hack/os2loop_login_hack.routing.yml @@ -0,0 +1,17 @@ +os2loop_login_hack.start: + path: '/os2loop-login-hack/start' + defaults: + _title: 'Start login hack' + _controller: '\Drupal\os2loop_login_hack\Controller\Os2loopLoginHackController::start' + methods: [POST] + requirements: + _role: 'anonymous' + +os2loop_login_hack.authenticate: + path: '/os2loop-login-hack/authenticate' + defaults: + _title: 'Authenticate' + _controller: '\Drupal\os2loop_login_hack\Controller\Os2loopLoginHackController::authenticate' + methods: [GET] + requirements: + _role: 'anonymous' diff --git a/web/profiles/custom/os2loop/modules/os2loop_login_hack/os2loop_login_hack.services.yml b/web/profiles/custom/os2loop/modules/os2loop_login_hack/os2loop_login_hack.services.yml new file mode 100644 index 000000000..0bc2889c1 --- /dev/null +++ b/web/profiles/custom/os2loop/modules/os2loop_login_hack/os2loop_login_hack.services.yml @@ -0,0 +1,4 @@ +services: + logger.channel.os2loop_login_hack: + parent: logger.channel_base + arguments: ['os2loop_login_hack'] diff --git a/web/profiles/custom/os2loop/modules/os2loop_login_hack/src/Controller/Os2loopLoginHackController.php b/web/profiles/custom/os2loop/modules/os2loop_login_hack/src/Controller/Os2loopLoginHackController.php new file mode 100644 index 000000000..9deb38dfe --- /dev/null +++ b/web/profiles/custom/os2loop/modules/os2loop_login_hack/src/Controller/Os2loopLoginHackController.php @@ -0,0 +1,167 @@ +userStorage = $entityTypeManager->getStorage('user'); + } + + /** + * Start user authentication. + */ + public function start(Request $request): Response { + try { + $data = json_decode($request->getContent(), associative: TRUE, flags: JSON_THROW_ON_ERROR); + $username = $data['username'] ?? NULL; + if (empty($username)) { + throw new BadRequestHttpException('Missing username'); + } + + $user = $this->loadUser($username); + if (empty($user)) { + // Don't disclose whether or not the user exists. + throw new BadRequestHttpException(); + } + + // Check that we can get userinfo. + $userinfo = $this->getUserinfo($user); + if (empty($userinfo)) { + throw new BadRequestHttpException(); + } + + // https://github.com/firebase/php-jwt?tab=readme-ov-file#example + $payload = [ + // Issued at. + 'iat' => $this->time->getRequestTime(), + // Expire af 60 seconds. + 'exp' => $this->time->getRequestTime() + 60, + 'username' => $username, + ]; + $jwt = JWT::encode($payload, self::JWT_KEY, 'HS256'); + + $url = Url::fromRoute('os2loop_login_hack.authenticate', [ + 'username' => $username, + 'jwt' => $jwt, + ])->setAbsolute()->toString(TRUE)->getGeneratedUrl(); + + return new JsonResponse([ + 'authenticate_url' => $url, + 'jwt' => $jwt, + ]); + } + catch (\Exception $exception) { + $this->logger->error('start: @message', ['@message' => $exception->getMessage(), $exception]); + throw new BadRequestException($exception->getMessage()); + } + } + + /** + * Authenticate user. + */ + public function authenticate(Request $request): Response { + try { + $username = $request->get('username'); + $jwt = $request->get('jwt'); + if (empty($username) || empty($jwt)) { + throw new BadRequestHttpException(); + } + + $payload = (array) JWT::decode($jwt, new Key(self::JWT_KEY, 'HS256')); + $username = $payload['username'] ?? NULL; + if (empty($username)) { + throw new BadRequestHttpException(); + } + + $user = $this->loadUser($username); + if (empty($user)) { + // Don't disclose whether or not the user exists. + throw new BadRequestHttpException(); + } + + $this->updateUser($user); + + user_login_finalize($user); + + $url = Url::fromRoute('')->setAbsolute()->toString(TRUE)->getGeneratedUrl(); + $this->messenger()->addStatus($this->t('Welcome @user.', ['@user' => $user->getDisplayName()])); + + return new TrustedRedirectResponse($url); + } + catch (\Exception $exception) { + $this->logger->error('start: @message', ['@message' => $exception->getMessage(), $exception]); + throw new BadRequestException($exception->getMessage()); + } + } + + /** + * Load user by username. + * + * @param string $username + * The username. + * + * @return \Drupal\user\Entity\User|null + * The user if any. + */ + private function loadUser(string $username): ?User { + $users = $this->userStorage->loadByProperties(['name' => $username]); + + return reset($users) ?: NULL; + } + + /** + * Update user with info from IdP. + */ + private function updateUser(User $user): User { + // $userinfo = $this->getUserinfo($user); + // @todo Update user. + return $user; + } + + /** + * Get user info from userinfo endpoint. + */ + private function getUserinfo(User $user): array { + return [ + 'name' => $user->getDisplayName(), + ]; + } + +} From 8391b7142ba3a262a96a70b1cd496383a421fa59 Mon Sep 17 00:00:00 2001 From: Mikkel Ricky Date: Tue, 2 Sep 2025 09:53:19 +0200 Subject: [PATCH 02/15] 5124: Debugging --- .../os2loop_login_hack.routing.yml | 16 ++++++++++++++-- .../Controller/Os2loopLoginHackController.php | 10 ++++++++++ 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/web/profiles/custom/os2loop/modules/os2loop_login_hack/os2loop_login_hack.routing.yml b/web/profiles/custom/os2loop/modules/os2loop_login_hack/os2loop_login_hack.routing.yml index ce5be46b5..770611db1 100644 --- a/web/profiles/custom/os2loop/modules/os2loop_login_hack/os2loop_login_hack.routing.yml +++ b/web/profiles/custom/os2loop/modules/os2loop_login_hack/os2loop_login_hack.routing.yml @@ -1,9 +1,9 @@ os2loop_login_hack.start: - path: '/os2loop-login-hack/start' + path: '/os2loop-cura-login/start' defaults: _title: 'Start login hack' _controller: '\Drupal\os2loop_login_hack\Controller\Os2loopLoginHackController::start' - methods: [POST] + methods: [GET, POST] requirements: _role: 'anonymous' @@ -15,3 +15,15 @@ os2loop_login_hack.authenticate: methods: [GET] requirements: _role: 'anonymous' + +os2loop_login_preview.show: + path: '/os2loop-login-hack/preview/show/{id}' +# options: +# parameters: +# id: '.+' + defaults: + _title: 'Preview' + _controller: '\Drupal\os2loop_login_hack\Controller\Os2loopPreviewContentController::show' + methods: [GET] + requirements: + _permission: 'access content' diff --git a/web/profiles/custom/os2loop/modules/os2loop_login_hack/src/Controller/Os2loopLoginHackController.php b/web/profiles/custom/os2loop/modules/os2loop_login_hack/src/Controller/Os2loopLoginHackController.php index 9deb38dfe..f2ee16b2c 100644 --- a/web/profiles/custom/os2loop/modules/os2loop_login_hack/src/Controller/Os2loopLoginHackController.php +++ b/web/profiles/custom/os2loop/modules/os2loop_login_hack/src/Controller/Os2loopLoginHackController.php @@ -49,6 +49,16 @@ public function __construct( */ public function start(Request $request): Response { try { + $this->logger->info('Request: @request', [ + '@request' => json_encode([ + 'method' => $request->getMethod(), + 'query' => $request->query->all(), + 'content' => (string) $request->getContent(), + ]), + ]); + + return new Response('https://example.com/cura-login'); + $data = json_decode($request->getContent(), associative: TRUE, flags: JSON_THROW_ON_ERROR); $username = $data['username'] ?? NULL; if (empty($username)) { From 94bef0baf9e0de95bf71ec6851beab32635cad32 Mon Sep 17 00:00:00 2001 From: Mikkel Ricky Date: Wed, 3 Sep 2025 13:09:38 +0200 Subject: [PATCH 03/15] 5124: Making progress --- .../.gitignore | 0 .../modules/os2loop_cura_login/README.md | 27 ++++ .../composer.json | 4 +- .../schema/os2loop_cura_login.schema.yml | 8 ++ .../os2loop_cura_login.info.yml | 9 ++ .../os2loop_cura_login.links.menu.yml | 6 + .../os2loop_cura_login.routing.yml | 25 ++++ .../os2loop_cura_login.services.yml | 4 + .../Os2loopCuraLoginController.php} | 62 ++++++--- .../Commands/Os2loopCuraLoginCommands.php | 85 +++++++++++++ .../src/Form/SettingsForm.php | 119 ++++++++++++++++++ .../os2loop_login_hack.info.yml | 7 -- .../os2loop_login_hack.routing.yml | 29 ----- .../os2loop_login_hack.services.yml | 4 - web/sites/default/settings.php | 3 + 15 files changed, 335 insertions(+), 57 deletions(-) rename web/profiles/custom/os2loop/modules/{os2loop_login_hack => os2loop_cura_login}/.gitignore (100%) create mode 100644 web/profiles/custom/os2loop/modules/os2loop_cura_login/README.md rename web/profiles/custom/os2loop/modules/{os2loop_login_hack => os2loop_cura_login}/composer.json (73%) create mode 100644 web/profiles/custom/os2loop/modules/os2loop_cura_login/config/schema/os2loop_cura_login.schema.yml create mode 100644 web/profiles/custom/os2loop/modules/os2loop_cura_login/os2loop_cura_login.info.yml create mode 100644 web/profiles/custom/os2loop/modules/os2loop_cura_login/os2loop_cura_login.links.menu.yml create mode 100644 web/profiles/custom/os2loop/modules/os2loop_cura_login/os2loop_cura_login.routing.yml create mode 100644 web/profiles/custom/os2loop/modules/os2loop_cura_login/os2loop_cura_login.services.yml rename web/profiles/custom/os2loop/modules/{os2loop_login_hack/src/Controller/Os2loopLoginHackController.php => os2loop_cura_login/src/Controller/Os2loopCuraLoginController.php} (67%) create mode 100644 web/profiles/custom/os2loop/modules/os2loop_cura_login/src/Drush/Commands/Os2loopCuraLoginCommands.php create mode 100644 web/profiles/custom/os2loop/modules/os2loop_cura_login/src/Form/SettingsForm.php delete mode 100644 web/profiles/custom/os2loop/modules/os2loop_login_hack/os2loop_login_hack.info.yml delete mode 100644 web/profiles/custom/os2loop/modules/os2loop_login_hack/os2loop_login_hack.routing.yml delete mode 100644 web/profiles/custom/os2loop/modules/os2loop_login_hack/os2loop_login_hack.services.yml diff --git a/web/profiles/custom/os2loop/modules/os2loop_login_hack/.gitignore b/web/profiles/custom/os2loop/modules/os2loop_cura_login/.gitignore similarity index 100% rename from web/profiles/custom/os2loop/modules/os2loop_login_hack/.gitignore rename to web/profiles/custom/os2loop/modules/os2loop_cura_login/.gitignore diff --git a/web/profiles/custom/os2loop/modules/os2loop_cura_login/README.md b/web/profiles/custom/os2loop/modules/os2loop_cura_login/README.md new file mode 100644 index 000000000..9c9c7ab5a --- /dev/null +++ b/web/profiles/custom/os2loop/modules/os2loop_cura_login/README.md @@ -0,0 +1,27 @@ +# Cura login + +Use `https://os2loop.example.com/os2loop-cura-login/start` as `linkURL` (or is it `formPostUrl`?) in the Cura link +configuration. + +## Example + +``` shell +curl "http://$(docker compose port nginx 8080)/os2loop-cura-login/start" +``` + + +``` shell +drush os2loop-cura-login:get-login-url --help +``` + +``` shell +drush --uri='http://nginx:8080' os2loop-cura-login:get-login-url test@example.com --secret=$(drush config:get --format string os2loop_cura_login.settings signing_secret --include-overridden) --algorithm=$(drush config:get --format string os2loop_cura_login.settings signing_algorithm --include-overridden) +``` + + +## Development and debugging + +``` php +# settings.local.php +$config['os2loop_cura_login.settings']['log_level'] = \Drupal\Core\Logger\RfcLogLevel::DEBUG; +``` diff --git a/web/profiles/custom/os2loop/modules/os2loop_login_hack/composer.json b/web/profiles/custom/os2loop/modules/os2loop_cura_login/composer.json similarity index 73% rename from web/profiles/custom/os2loop/modules/os2loop_login_hack/composer.json rename to web/profiles/custom/os2loop/modules/os2loop_cura_login/composer.json index dc3ef3cc9..362fc2f77 100644 --- a/web/profiles/custom/os2loop/modules/os2loop_login_hack/composer.json +++ b/web/profiles/custom/os2loop/modules/os2loop_cura_login/composer.json @@ -1,6 +1,6 @@ { - "name": "os2loop/os2loop_login_hack", - "description": "drupal/os2loop_login_hack", + "name": "os2loop/os2loop_cura_login", + "description": "drupal/os2loop_cura_login", "license": "GPL-2.0+", "type": "os2loop-custom-module", "authors": [ diff --git a/web/profiles/custom/os2loop/modules/os2loop_cura_login/config/schema/os2loop_cura_login.schema.yml b/web/profiles/custom/os2loop/modules/os2loop_cura_login/config/schema/os2loop_cura_login.schema.yml new file mode 100644 index 000000000..710fcce5f --- /dev/null +++ b/web/profiles/custom/os2loop/modules/os2loop_cura_login/config/schema/os2loop_cura_login.schema.yml @@ -0,0 +1,8 @@ +# Schema for the configuration files of the OS2Loop Cura login module. +os2loop_cura_login.settings: + type: config_object + label: 'OS2Loop Cura login settings' + mapping: + example: + type: string + label: 'Example' diff --git a/web/profiles/custom/os2loop/modules/os2loop_cura_login/os2loop_cura_login.info.yml b/web/profiles/custom/os2loop/modules/os2loop_cura_login/os2loop_cura_login.info.yml new file mode 100644 index 000000000..69b4af90d --- /dev/null +++ b/web/profiles/custom/os2loop/modules/os2loop_cura_login/os2loop_cura_login.info.yml @@ -0,0 +1,9 @@ +name: 'OS2Loop Cura login' +type: module +description: 'OS2Loop Cura login' +package: 'OS2Loop' +core_version_requirement: ^10 || ^11 +dependencies: + - drupal:user + +configure: os2loop_cura_login.settings diff --git a/web/profiles/custom/os2loop/modules/os2loop_cura_login/os2loop_cura_login.links.menu.yml b/web/profiles/custom/os2loop/modules/os2loop_cura_login/os2loop_cura_login.links.menu.yml new file mode 100644 index 000000000..7239e056e --- /dev/null +++ b/web/profiles/custom/os2loop/modules/os2loop_cura_login/os2loop_cura_login.links.menu.yml @@ -0,0 +1,6 @@ +os2loop_cura_login.settings: + title: 'OS2Loop Cura login settings' + route_name: os2loop_cura_login.settings + description: 'Configure OS2Loop Cura login settings' + parent: os2loop.group.admin + weight: 100 diff --git a/web/profiles/custom/os2loop/modules/os2loop_cura_login/os2loop_cura_login.routing.yml b/web/profiles/custom/os2loop/modules/os2loop_cura_login/os2loop_cura_login.routing.yml new file mode 100644 index 000000000..0380b4515 --- /dev/null +++ b/web/profiles/custom/os2loop/modules/os2loop_cura_login/os2loop_cura_login.routing.yml @@ -0,0 +1,25 @@ +os2loop_cura_login.start: + path: '/os2loop-cura-login/start' + defaults: + _title: 'Start login hack' + _controller: '\Drupal\os2loop_cura_login\Controller\Os2loopCuraLoginController::start' + methods: [GET, POST] + requirements: + _role: 'anonymous' + +os2loop_cura_login.authenticate: + path: '/os2loop-login-hack/authenticate' + defaults: + _title: 'Authenticate' + _controller: '\Drupal\os2loop_cura_login\Controller\Os2loopCuraLoginController::authenticate' + methods: [GET] + requirements: + _role: 'anonymous' + +os2loop_cura_login.settings: + path: '/admin/config/os2loop/os2loop_cura_login/settings' + defaults: + _form: '\Drupal\os2loop_cura_login\Form\SettingsForm' + _title: 'OS2Loop Cura login settings' + requirements: + _permission: 'administer site settings' diff --git a/web/profiles/custom/os2loop/modules/os2loop_cura_login/os2loop_cura_login.services.yml b/web/profiles/custom/os2loop/modules/os2loop_cura_login/os2loop_cura_login.services.yml new file mode 100644 index 000000000..3f350d095 --- /dev/null +++ b/web/profiles/custom/os2loop/modules/os2loop_cura_login/os2loop_cura_login.services.yml @@ -0,0 +1,4 @@ +services: + logger.channel.os2loop_cura_login: + parent: logger.channel_base + arguments: ['os2loop_cura_login'] diff --git a/web/profiles/custom/os2loop/modules/os2loop_login_hack/src/Controller/Os2loopLoginHackController.php b/web/profiles/custom/os2loop/modules/os2loop_cura_login/src/Controller/Os2loopCuraLoginController.php similarity index 67% rename from web/profiles/custom/os2loop/modules/os2loop_login_hack/src/Controller/Os2loopLoginHackController.php rename to web/profiles/custom/os2loop/modules/os2loop_cura_login/src/Controller/Os2loopCuraLoginController.php index f2ee16b2c..b2d799065 100644 --- a/web/profiles/custom/os2loop/modules/os2loop_login_hack/src/Controller/Os2loopLoginHackController.php +++ b/web/profiles/custom/os2loop/modules/os2loop_cura_login/src/Controller/Os2loopCuraLoginController.php @@ -2,11 +2,12 @@ declare(strict_types=1); -namespace Drupal\os2loop_login_hack\Controller; +namespace Drupal\os2loop_cura_login\Controller; use Drupal\Component\Datetime\TimeInterface; +use Drupal\Core\Config\ImmutableConfig; use Drupal\Core\Controller\ControllerBase; -use Drupal\Core\Entity\EntityTypeManagerInterface; +use Drupal\Core\Logger\RfcLogLevel; use Drupal\Core\Routing\TrustedRedirectResponse; use Drupal\Core\Url; use Drupal\user\Entity\User; @@ -14,6 +15,8 @@ use Firebase\JWT\JWT; use Firebase\JWT\Key; use Psr\Log\LoggerInterface; +use Psr\Log\LoggerTrait; +use Psr\Log\LogLevel; use Symfony\Component\DependencyInjection\Attribute\Autowire; use Symfony\Component\HttpFoundation\Exception\BadRequestException; use Symfony\Component\HttpFoundation\JsonResponse; @@ -22,26 +25,33 @@ use Symfony\Component\HttpKernel\Exception\BadRequestHttpException; /** - * Returns responses for os2loop_login_hack routes. + * Returns responses for os2loop_cura_login routes. */ -final class Os2loopLoginHackController extends ControllerBase { - private const JWT_KEY = 'os2loop_login_hack'; +final class Os2loopCuraLoginController extends ControllerBase { + use LoggerTrait; + + private const JWT_KEY = 'os2loop_cura_login'; /** * The user storage. */ private readonly UserStorageInterface $userStorage; + /** + * The module config. + */ + private readonly ImmutableConfig $config; + /** * Constructor. */ public function __construct( - EntityTypeManagerInterface $entityTypeManager, private readonly TimeInterface $time, - #[Autowire(service: 'logger.channel.os2loop_login_hack')] + #[Autowire(service: 'logger.channel.os2loop_cura_login')] private readonly LoggerInterface $logger, ) { - $this->userStorage = $entityTypeManager->getStorage('user'); + $this->userStorage = $this->entityTypeManager()->getStorage('user'); + $this->config = $this->config('os2loop_cura_login.settings'); } /** @@ -49,7 +59,7 @@ public function __construct( */ public function start(Request $request): Response { try { - $this->logger->info('Request: @request', [ + $this->info('Request: @request', [ '@request' => json_encode([ 'method' => $request->getMethod(), 'query' => $request->query->all(), @@ -57,10 +67,13 @@ public function start(Request $request): Response { ]), ]); - return new Response('https://example.com/cura-login'); + $jwt = Request::METHOD_POST === $request->getMethod() + ? $request->getContent() + : $request->query->getString($this->config->get('token_param_name') ?? 'token'); + + $payload = (array) JWT::decode($jwt, new Key($this->config->get('signing_secret'), $this->config->get('signing_algorithm'))); - $data = json_decode($request->getContent(), associative: TRUE, flags: JSON_THROW_ON_ERROR); - $username = $data['username'] ?? NULL; + $username = $payload['username'] ?? NULL; if (empty($username)) { throw new BadRequestHttpException('Missing username'); } @@ -87,7 +100,7 @@ public function start(Request $request): Response { ]; $jwt = JWT::encode($payload, self::JWT_KEY, 'HS256'); - $url = Url::fromRoute('os2loop_login_hack.authenticate', [ + $url = Url::fromRoute('os2loop_cura_login.authenticate', [ 'username' => $username, 'jwt' => $jwt, ])->setAbsolute()->toString(TRUE)->getGeneratedUrl(); @@ -98,7 +111,7 @@ public function start(Request $request): Response { ]); } catch (\Exception $exception) { - $this->logger->error('start: @message', ['@message' => $exception->getMessage(), $exception]); + $this->error('start: @message', ['@message' => $exception->getMessage(), $exception]); throw new BadRequestException($exception->getMessage()); } } @@ -136,7 +149,7 @@ public function authenticate(Request $request): Response { return new TrustedRedirectResponse($url); } catch (\Exception $exception) { - $this->logger->error('start: @message', ['@message' => $exception->getMessage(), $exception]); + $this->error('start: @message', ['@message' => $exception->getMessage(), $exception]); throw new BadRequestException($exception->getMessage()); } } @@ -174,4 +187,23 @@ private function getUserinfo(User $user): array { ]; } + public function log($level, \Stringable|string $message, array $context = []): void + { + // Lifted from LoggerChannel + $levels = [ + LogLevel::EMERGENCY => RfcLogLevel::EMERGENCY, + LogLevel::ALERT => RfcLogLevel::ALERT, + LogLevel::CRITICAL => RfcLogLevel::CRITICAL, + LogLevel::ERROR => RfcLogLevel::ERROR, + LogLevel::WARNING => RfcLogLevel::WARNING, + LogLevel::NOTICE => RfcLogLevel::NOTICE, + LogLevel::INFO => RfcLogLevel::INFO, + LogLevel::DEBUG => RfcLogLevel::DEBUG, + ]; + $rfcLogLevel = $levels[$level] ?? RfcLogLevel::ERROR; + if ((int)$this->config->get('log_level') >= $rfcLogLevel) { + $this->logger->log($level, $message, $context); + } + } + } diff --git a/web/profiles/custom/os2loop/modules/os2loop_cura_login/src/Drush/Commands/Os2loopCuraLoginCommands.php b/web/profiles/custom/os2loop/modules/os2loop_cura_login/src/Drush/Commands/Os2loopCuraLoginCommands.php new file mode 100644 index 000000000..f4d49c7b6 --- /dev/null +++ b/web/profiles/custom/os2loop/modules/os2loop_cura_login/src/Drush/Commands/Os2loopCuraLoginCommands.php @@ -0,0 +1,85 @@ + null, + 'secret' => null, + 'algorithm' => 'HS256', + ] + ) { + // https://github.com/firebase/php-jwt?tab=readme-ov-file#example + $payload = [ + // Issued at. + 'iat' => $this->time->getRequestTime(), + // Expire af 60 seconds. + 'exp' => $this->time->getRequestTime() + 60, + 'username' => $username, + ]; + $jwt = JWT::encode($payload, $options['secret'], $options['algorithm']); + + $routeParameters = []; + $requestOptions = []; + if ($name = $options['get']) { + $method = Request::METHOD_GET; + $routeParameters[$name] = $jwt; + } else { + $method = Request::METHOD_POST; + $requestOptions['body'] = $jwt; + } + $url = Url::fromRoute('os2loop_cura_login.start', $routeParameters)->setAbsolute()->toString(true)->getGeneratedUrl(); + $this->io()->writeln($method === Request::METHOD_POST + ? sprintf('POST\'ing to %s', $url) + : sprintf('GET\'ing %s', $url), + ); + $request = $this->httpClient->request($method, $url, $requestOptions); + + header('content-type: text/plain'); + echo var_export([ + $url, + $request->getStatusCode(), + $request->getBody()->getContents(), + ], true); + die(__FILE__ . ':' . __LINE__ . ':' . __METHOD__); + } + +} diff --git a/web/profiles/custom/os2loop/modules/os2loop_cura_login/src/Form/SettingsForm.php b/web/profiles/custom/os2loop/modules/os2loop_cura_login/src/Form/SettingsForm.php new file mode 100644 index 000000000..7ca872d7a --- /dev/null +++ b/web/profiles/custom/os2loop/modules/os2loop_cura_login/src/Form/SettingsForm.php @@ -0,0 +1,119 @@ +config('os2loop_cura_login.settings'); + $form['signing_algorithm'] = [ + '#required' => TRUE, + '#type' => 'select', + '#options' => [ + 'HS256' => 'HS256', + 'HS384' => 'HS384', + 'HS512' => 'HS512', + ], + '#title' => $this->t('Signing algorithm'), + '#default_value' => $config->get('signing_algorithm'), + ]; + + $hasSigningSecret = !empty($config->get('signing_secret')); + + $form['signing_secret'] = [ + '#type' => 'textfield', + '#size' => 128, + '#required' => $hasSigningSecret, + '#title' => $this->t('Signing secret'), + '#default_value' => $config->get('signing_secret'), + '#description' => !$hasSigningSecret + ? $this->t('Save the configuration to generate a random secret.') + : '', + ]; + + $form['generate_new_secret'] = [ + '#title' => $this->t('Generate new secret'), + '#type' => 'checkbox', + '#default_value' => !$hasSigningSecret, + '#description' => $this->t('Check this to generate a new random secret'), + ]; + + $form['token_param_name'] = [ + '#type' => 'textfield', + '#title' => $this->t('Token param name'), + '#default_value' => $config->get('token_param_name'), + '#description' => $this->t('Query string param name used for JWT token on GET request'), + ]; + + $form['log_level'] = [ + '#type' => 'select', + '#options' => RfcLogLevel::getLevels(), + '#title' => $this->t('Log level'), + '#default_value' => $config->get('log_level') ?? RfcLogLevel::ERROR, + ]; + + return parent::buildForm($form, $form_state); + } + + /** + * {@inheritdoc} + */ + public function validateForm(array &$form, FormStateInterface $form_state): void { + // @todo Validate the form here. + // Example: + // @code + // if ($form_state->getValue('example') === 'wrong') { + // $form_state->setErrorByName( + // 'message', + // $this->t('The value is not correct.'), + // ); + // } + // @endcode + parent::validateForm($form, $form_state); + } + + /** + * {@inheritdoc} + */ + public function submitForm(array &$form, FormStateInterface $form_state): void { + $secret = $form_state->getValue('signing_secret'); + if ($form_state->getValue('generate_new_secret')) { + $secret = base64_encode((new Random())->string(64)); + } + $this->config('os2loop_cura_login.settings') + ->set('signing_algorithm', $form_state->getValue('signing_algorithm')) + ->set('signing_secret', $secret) + ->set('token_param_name', $form_state->getValue('token_param_name')) + ->set('log_level', $form_state->getValue('log_level')) + ->save(); + parent::submitForm($form, $form_state); + } + +} diff --git a/web/profiles/custom/os2loop/modules/os2loop_login_hack/os2loop_login_hack.info.yml b/web/profiles/custom/os2loop/modules/os2loop_login_hack/os2loop_login_hack.info.yml deleted file mode 100644 index 8d23c2764..000000000 --- a/web/profiles/custom/os2loop/modules/os2loop_login_hack/os2loop_login_hack.info.yml +++ /dev/null @@ -1,7 +0,0 @@ -name: 'os2loop_login_hack' -type: module -description: 'os2loop_login_hack' -package: Custom -core_version_requirement: ^10 || ^11 -dependencies: - - drupal:user diff --git a/web/profiles/custom/os2loop/modules/os2loop_login_hack/os2loop_login_hack.routing.yml b/web/profiles/custom/os2loop/modules/os2loop_login_hack/os2loop_login_hack.routing.yml deleted file mode 100644 index 770611db1..000000000 --- a/web/profiles/custom/os2loop/modules/os2loop_login_hack/os2loop_login_hack.routing.yml +++ /dev/null @@ -1,29 +0,0 @@ -os2loop_login_hack.start: - path: '/os2loop-cura-login/start' - defaults: - _title: 'Start login hack' - _controller: '\Drupal\os2loop_login_hack\Controller\Os2loopLoginHackController::start' - methods: [GET, POST] - requirements: - _role: 'anonymous' - -os2loop_login_hack.authenticate: - path: '/os2loop-login-hack/authenticate' - defaults: - _title: 'Authenticate' - _controller: '\Drupal\os2loop_login_hack\Controller\Os2loopLoginHackController::authenticate' - methods: [GET] - requirements: - _role: 'anonymous' - -os2loop_login_preview.show: - path: '/os2loop-login-hack/preview/show/{id}' -# options: -# parameters: -# id: '.+' - defaults: - _title: 'Preview' - _controller: '\Drupal\os2loop_login_hack\Controller\Os2loopPreviewContentController::show' - methods: [GET] - requirements: - _permission: 'access content' diff --git a/web/profiles/custom/os2loop/modules/os2loop_login_hack/os2loop_login_hack.services.yml b/web/profiles/custom/os2loop/modules/os2loop_login_hack/os2loop_login_hack.services.yml deleted file mode 100644 index 0bc2889c1..000000000 --- a/web/profiles/custom/os2loop/modules/os2loop_login_hack/os2loop_login_hack.services.yml +++ /dev/null @@ -1,4 +0,0 @@ -services: - logger.channel.os2loop_login_hack: - parent: logger.channel_base - arguments: ['os2loop_login_hack'] diff --git a/web/sites/default/settings.php b/web/sites/default/settings.php index e0e2ce8cc..cdd0c0fb9 100644 --- a/web/sites/default/settings.php +++ b/web/sites/default/settings.php @@ -780,6 +780,9 @@ $settings['skip_permissions_hardening'] = TRUE; +// https://www.drupal.org/node/3079028 +$settings['config_exclude_modules'] = ['os2loop_cura_login']; + /** * Load local development override configuration, if available. * From 37c677d73f3ba21cc232f7118c50b25711d792e1 Mon Sep 17 00:00:00 2001 From: Mikkel Ricky Date: Wed, 3 Sep 2025 13:37:23 +0200 Subject: [PATCH 04/15] 5124: Cleaned up composer stuff --- composer.json | 15 ++- composer.lock | 96 ++++++++++++++++++- .../modules/os2loop_cura_login/composer.json | 2 +- 3 files changed, 107 insertions(+), 6 deletions(-) diff --git a/composer.json b/composer.json index 839a89d54..9f2861861 100644 --- a/composer.json +++ b/composer.json @@ -60,7 +60,8 @@ "drupal/viewsreference": "^2.0@beta", "drupal/xls_serialization": "^2.0", "drush/drush": "^13.0", - "jjj/chosen": "^2.2" + "jjj/chosen": "^2.2", + "os2loop/os2loop_cura_login": "^1.0" }, "require-dev": { "dealerdirect/phpcodesniffer-composer-installer": "^1.0", @@ -102,11 +103,21 @@ "type": "vcs", "url": "https://git.drupalcode.org/project/theme_switcher" }, + "os2loop/os2loop_cura_login": { + "type": "path", + "url": "web/profiles/custom/os2loop/modules/os2loop_cura_login", + "options": { + "symlink": true, + "versions": { + "os2loop/os2loop_cura_login": "1.0" + } + } + }, "os2loop/os2loop_fixtures": { "type": "path", "url": "web/profiles/custom/os2loop/modules/os2loop_fixtures", "options": { - "symlink": false, + "symlink": true, "versions": { "os2loop/os2loop_fixtures": "1.0" } diff --git a/composer.lock b/composer.lock index f04cf4528..260941d53 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "6ed4fdb66736874a4e9be667abfee23c", + "content-hash": "159422ad2d499c1c75bd47b85cdb7e83", "packages": [ { "name": "asm89/stack-cors", @@ -5486,6 +5486,69 @@ }, "time": "2024-11-01T03:51:45+00:00" }, + { + "name": "firebase/php-jwt", + "version": "v6.11.1", + "source": { + "type": "git", + "url": "https://github.com/firebase/php-jwt.git", + "reference": "d1e91ecf8c598d073d0995afa8cd5c75c6e19e66" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/firebase/php-jwt/zipball/d1e91ecf8c598d073d0995afa8cd5c75c6e19e66", + "reference": "d1e91ecf8c598d073d0995afa8cd5c75c6e19e66", + "shasum": "" + }, + "require": { + "php": "^8.0" + }, + "require-dev": { + "guzzlehttp/guzzle": "^7.4", + "phpspec/prophecy-phpunit": "^2.0", + "phpunit/phpunit": "^9.5", + "psr/cache": "^2.0||^3.0", + "psr/http-client": "^1.0", + "psr/http-factory": "^1.0" + }, + "suggest": { + "ext-sodium": "Support EdDSA (Ed25519) signatures", + "paragonie/sodium_compat": "Support EdDSA (Ed25519) signatures when libsodium is not present" + }, + "type": "library", + "autoload": { + "psr-4": { + "Firebase\\JWT\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Neuman Vong", + "email": "neuman+pear@twilio.com", + "role": "Developer" + }, + { + "name": "Anant Narayanan", + "email": "anant@php.net", + "role": "Developer" + } + ], + "description": "A simple library to encode and decode JSON Web Tokens (JWT) in PHP. Should conform to the current spec.", + "homepage": "https://github.com/firebase/php-jwt", + "keywords": [ + "jwt", + "php" + ], + "support": { + "issues": "https://github.com/firebase/php-jwt/issues", + "source": "https://github.com/firebase/php-jwt/tree/v6.11.1" + }, + "time": "2025-04-09T20:32:01+00:00" + }, { "name": "grasmash/expander", "version": "3.0.1", @@ -6665,6 +6728,33 @@ }, "time": "2025-05-31T08:24:38+00:00" }, + { + "name": "os2loop/os2loop_cura_login", + "version": "1.0", + "dist": { + "type": "path", + "url": "web/profiles/custom/os2loop/modules/os2loop_cura_login", + "reference": "8b83fb8dde0ee70b54e7f2454784ba2c311bb67b" + }, + "require": { + "firebase/php-jwt": "^6.11" + }, + "type": "os2loop-custom-module", + "license": [ + "GPL-2.0+" + ], + "authors": [ + { + "name": "Mikkel Ricky", + "email": "rimi@aarhus.dk" + } + ], + "description": "drupal/os2loop_cura_login", + "transport-options": { + "symlink": true, + "relative": true + } + }, { "name": "pear/archive_tar", "version": "1.5.0", @@ -13872,7 +13962,7 @@ "dist": { "type": "path", "url": "web/profiles/custom/os2loop/modules/os2loop_fixtures", - "reference": "4f02f1c5fabeca7800285dd74124a55528892d54" + "reference": "7b31ca9484dad6913a7e991e0ac128bc284a2b34" }, "require": { "drupal/content_fixtures": "^3.2", @@ -13890,7 +13980,7 @@ ], "description": "OS2Loop Fixtures.", "transport-options": { - "symlink": false, + "symlink": true, "relative": true } }, diff --git a/web/profiles/custom/os2loop/modules/os2loop_cura_login/composer.json b/web/profiles/custom/os2loop/modules/os2loop_cura_login/composer.json index 362fc2f77..8edba29bf 100644 --- a/web/profiles/custom/os2loop/modules/os2loop_cura_login/composer.json +++ b/web/profiles/custom/os2loop/modules/os2loop_cura_login/composer.json @@ -1,8 +1,8 @@ { "name": "os2loop/os2loop_cura_login", "description": "drupal/os2loop_cura_login", - "license": "GPL-2.0+", "type": "os2loop-custom-module", + "license": "GPL-2.0+", "authors": [ { "name": "Mikkel Ricky", From 4f236869bbd0ea06c9ef3f0bb3dbcafcf17b766c Mon Sep 17 00:00:00 2001 From: Mikkel Ricky Date: Wed, 3 Sep 2025 13:49:09 +0200 Subject: [PATCH 05/15] 5124: Improved stuff --- .../modules/os2loop_cura_login/README.md | 2 - .../os2loop_cura_login.routing.yml | 4 +- .../Controller/Os2loopCuraLoginController.php | 40 +++++++++++++++++-- .../src/Form/SettingsForm.php | 17 ++++++++ 4 files changed, 56 insertions(+), 7 deletions(-) diff --git a/web/profiles/custom/os2loop/modules/os2loop_cura_login/README.md b/web/profiles/custom/os2loop/modules/os2loop_cura_login/README.md index 9c9c7ab5a..72d7c9714 100644 --- a/web/profiles/custom/os2loop/modules/os2loop_cura_login/README.md +++ b/web/profiles/custom/os2loop/modules/os2loop_cura_login/README.md @@ -9,7 +9,6 @@ configuration. curl "http://$(docker compose port nginx 8080)/os2loop-cura-login/start" ``` - ``` shell drush os2loop-cura-login:get-login-url --help ``` @@ -18,7 +17,6 @@ drush os2loop-cura-login:get-login-url --help drush --uri='http://nginx:8080' os2loop-cura-login:get-login-url test@example.com --secret=$(drush config:get --format string os2loop_cura_login.settings signing_secret --include-overridden) --algorithm=$(drush config:get --format string os2loop_cura_login.settings signing_algorithm --include-overridden) ``` - ## Development and debugging ``` php diff --git a/web/profiles/custom/os2loop/modules/os2loop_cura_login/os2loop_cura_login.routing.yml b/web/profiles/custom/os2loop/modules/os2loop_cura_login/os2loop_cura_login.routing.yml index 0380b4515..c92d9ef60 100644 --- a/web/profiles/custom/os2loop/modules/os2loop_cura_login/os2loop_cura_login.routing.yml +++ b/web/profiles/custom/os2loop/modules/os2loop_cura_login/os2loop_cura_login.routing.yml @@ -1,7 +1,7 @@ os2loop_cura_login.start: path: '/os2loop-cura-login/start' defaults: - _title: 'Start login hack' + _title: 'Start Cura login' _controller: '\Drupal\os2loop_cura_login\Controller\Os2loopCuraLoginController::start' methods: [GET, POST] requirements: @@ -10,7 +10,7 @@ os2loop_cura_login.start: os2loop_cura_login.authenticate: path: '/os2loop-login-hack/authenticate' defaults: - _title: 'Authenticate' + _title: 'Authenticate with Cura login' _controller: '\Drupal\os2loop_cura_login\Controller\Os2loopCuraLoginController::authenticate' methods: [GET] requirements: diff --git a/web/profiles/custom/os2loop/modules/os2loop_cura_login/src/Controller/Os2loopCuraLoginController.php b/web/profiles/custom/os2loop/modules/os2loop_cura_login/src/Controller/Os2loopCuraLoginController.php index b2d799065..88fa8c89b 100644 --- a/web/profiles/custom/os2loop/modules/os2loop_cura_login/src/Controller/Os2loopCuraLoginController.php +++ b/web/profiles/custom/os2loop/modules/os2loop_cura_login/src/Controller/Os2loopCuraLoginController.php @@ -59,11 +59,15 @@ public function __construct( */ public function start(Request $request): Response { try { - $this->info('Request: @request', [ - '@request' => json_encode([ + $content = NULL; + try { + $content = (string) $request->getContent(); + } catch (\Exception) {} + $this->debug('@debug', [ + '@debug' => json_encode([ 'method' => $request->getMethod(), 'query' => $request->query->all(), - 'content' => (string) $request->getContent(), + 'content' => $content, ]), ]); @@ -71,14 +75,37 @@ public function start(Request $request): Response { ? $request->getContent() : $request->query->getString($this->config->get('token_param_name') ?? 'token'); + $this->debug('@debug', [ + '@debug' => json_encode([ + 'jwt' => $jwt, + ]), + ]); + + if (empty($jwt)) { + throw new BadRequestHttpException('Missing or empty JWT'); + } + $payload = (array) JWT::decode($jwt, new Key($this->config->get('signing_secret'), $this->config->get('signing_algorithm'))); + $this->debug('@debug', [ + '@debug' => json_encode([ + 'payload' => $payload, + ]), + ]); + $username = $payload['username'] ?? NULL; if (empty($username)) { throw new BadRequestHttpException('Missing username'); } $user = $this->loadUser($username); + + $this->debug('@debug', [ + '@debug' => json_encode([ + 'user' => $user, + ]), + ]); + if (empty($user)) { // Don't disclose whether or not the user exists. throw new BadRequestHttpException(); @@ -86,6 +113,13 @@ public function start(Request $request): Response { // Check that we can get userinfo. $userinfo = $this->getUserinfo($user); + + $this->debug('@debug', [ + '@debug' => json_encode([ + 'userinfo' => $userinfo, + ]), + ]); + if (empty($userinfo)) { throw new BadRequestHttpException(); } diff --git a/web/profiles/custom/os2loop/modules/os2loop_cura_login/src/Form/SettingsForm.php b/web/profiles/custom/os2loop/modules/os2loop_cura_login/src/Form/SettingsForm.php index 7ca872d7a..cbb2e469f 100644 --- a/web/profiles/custom/os2loop/modules/os2loop_cura_login/src/Form/SettingsForm.php +++ b/web/profiles/custom/os2loop/modules/os2loop_cura_login/src/Form/SettingsForm.php @@ -8,6 +8,7 @@ use Drupal\Core\Form\ConfigFormBase; use Drupal\Core\Form\FormStateInterface; use Drupal\Core\Logger\RfcLogLevel; +use Drupal\Core\Url; /** * Configure OS2Loop Cura login settings for this site. @@ -79,6 +80,22 @@ public function buildForm(array $form, FormStateInterface $form_state): array { '#default_value' => $config->get('log_level') ?? RfcLogLevel::ERROR, ]; + $authenticationStartUrl = Url::fromRoute('os2loop_cura_login.start')->setAbsolute()->toString(true)->getGeneratedUrl(); + $form['info'] = [ + '#theme' => 'item_list', + '#items' => [ + '#markup' => $this->t('Use :url as linkURL.', [':url' => $authenticationStartUrl]), + ], + ]; + + if ($name = $config->get('token_param_name')) { + $authenticationStartUrl = Url::fromRoute('os2loop_cura_login.start', [$name => '…'])->setAbsolute()->toString(true)->getGeneratedUrl(); + $authenticationStartUrl = str_replace(urlencode('…'), '…', $authenticationStartUrl); + $form['info']['#items'][] = [ + '#markup' => $this->t('Use :url as linkURL for GET.', [':url' => $authenticationStartUrl]), + ]; + } + return parent::buildForm($form, $form_state); } From 08dec18f44cb197604ceaa5409725cef655de7be Mon Sep 17 00:00:00 2001 From: Mikkel Ricky Date: Wed, 3 Sep 2025 14:57:04 +0200 Subject: [PATCH 06/15] 5124: Cleaned up --- CHANGELOG.md | 3 ++ README.md | 4 +- .../modules/os2loop_cura_login/README.md | 32 ++++++++++++ .../os2loop_cura_login.info.yml | 6 +-- .../os2loop_cura_login.links.menu.yml | 4 +- .../os2loop_cura_login.routing.yml | 27 ++++++---- .../os2loop_cura_login.services.yml | 2 +- .../Controller/Os2loopCuraLoginController.php | 42 ++++++++++----- .../Commands/Os2loopCuraLoginCommands.php | 20 +++---- .../src/Form/SettingsForm.php | 52 +++++++++---------- 10 files changed, 123 insertions(+), 69 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8e2d014d0..96df587bd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,9 @@ Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] +- [PR-375](https://github.com/itk-dev/os2loop/pull/375) + Added Cura login module + ## [1.2.5] - [374](https://github.com/itk-dev/os2loop/pull/374) diff --git a/README.md b/README.md index 0f09a434c..2d6e86d28 100644 --- a/README.md +++ b/README.md @@ -140,8 +140,8 @@ for further details. ## Coding standards ```sh -docker compose exec phpfpm composer coding-standards-apply -docker compose exec phpfpm composer coding-standards-check +docker compose exec phpfpm vendor/bin/phpcs +docker compose exec phpfpm vendor/bin/phpcbf ``` ```sh diff --git a/web/profiles/custom/os2loop/modules/os2loop_cura_login/README.md b/web/profiles/custom/os2loop/modules/os2loop_cura_login/README.md index 72d7c9714..37ef9c098 100644 --- a/web/profiles/custom/os2loop/modules/os2loop_cura_login/README.md +++ b/web/profiles/custom/os2loop/modules/os2loop_cura_login/README.md @@ -3,6 +3,30 @@ Use `https://os2loop.example.com/os2loop-cura-login/start` as `linkURL` (or is it `formPostUrl`?) in the Cura link configuration. +## How does it work? + +Cura will make a `POST` `multipart/form-data` request to `/os2loop-cura-login/start` with a `payload` parameter containing +a JWT like + +``` json +{ + "header" : { + "alg" : "HS256" + }, + "payload" : { + "brugerId" : "az…", + "organisationsNavn" : "Digitalisering", + "brugerensNavn" : "…", + "sorid" : "aarhus", + "exp" : 1756978913 + }, + "signature" : "…" +} +``` + +However, it can also make a `GET` request to `/os2loop-cura-login/start/{payload}` or +`/os2loop-cura-login/start/payload={payload}`. + ## Example ``` shell @@ -23,3 +47,11 @@ drush --uri='http://nginx:8080' os2loop-cura-login:get-login-url test@example.co # settings.local.php $config['os2loop_cura_login.settings']['log_level'] = \Drupal\Core\Logger\RfcLogLevel::DEBUG; ``` + +Run + +``` shell +drush config:get os2loop_cura_login.settings --include-overridden +``` + +to show the current module config. diff --git a/web/profiles/custom/os2loop/modules/os2loop_cura_login/os2loop_cura_login.info.yml b/web/profiles/custom/os2loop/modules/os2loop_cura_login/os2loop_cura_login.info.yml index 69b4af90d..6914d3d54 100644 --- a/web/profiles/custom/os2loop/modules/os2loop_cura_login/os2loop_cura_login.info.yml +++ b/web/profiles/custom/os2loop/modules/os2loop_cura_login/os2loop_cura_login.info.yml @@ -1,7 +1,7 @@ -name: 'OS2Loop Cura login' +name: "OS2Loop Cura login" type: module -description: 'OS2Loop Cura login' -package: 'OS2Loop' +description: "OS2Loop Cura login" +package: "OS2Loop" core_version_requirement: ^10 || ^11 dependencies: - drupal:user diff --git a/web/profiles/custom/os2loop/modules/os2loop_cura_login/os2loop_cura_login.links.menu.yml b/web/profiles/custom/os2loop/modules/os2loop_cura_login/os2loop_cura_login.links.menu.yml index 7239e056e..2bd579c0f 100644 --- a/web/profiles/custom/os2loop/modules/os2loop_cura_login/os2loop_cura_login.links.menu.yml +++ b/web/profiles/custom/os2loop/modules/os2loop_cura_login/os2loop_cura_login.links.menu.yml @@ -1,6 +1,6 @@ os2loop_cura_login.settings: - title: 'OS2Loop Cura login settings' + title: "OS2Loop Cura login settings" route_name: os2loop_cura_login.settings - description: 'Configure OS2Loop Cura login settings' + description: "Configure OS2Loop Cura login settings" parent: os2loop.group.admin weight: 100 diff --git a/web/profiles/custom/os2loop/modules/os2loop_cura_login/os2loop_cura_login.routing.yml b/web/profiles/custom/os2loop/modules/os2loop_cura_login/os2loop_cura_login.routing.yml index c92d9ef60..825034a2d 100644 --- a/web/profiles/custom/os2loop/modules/os2loop_cura_login/os2loop_cura_login.routing.yml +++ b/web/profiles/custom/os2loop/modules/os2loop_cura_login/os2loop_cura_login.routing.yml @@ -1,25 +1,34 @@ os2loop_cura_login.start: - path: '/os2loop-cura-login/start' + path: "/os2loop-cura-login/start" defaults: - _title: 'Start Cura login' + _title: "Start Cura login with POST or GET" _controller: '\Drupal\os2loop_cura_login\Controller\Os2loopCuraLoginController::start' methods: [GET, POST] requirements: - _role: 'anonymous' + _role: "anonymous" + +os2loop_cura_login.start_get_jwt: + path: "/os2loop-cura-login/start/{jwt}" + defaults: + _title: "Start Cura login with JWT in request path" + _controller: '\Drupal\os2loop_cura_login\Controller\Os2loopCuraLoginController::start' + methods: [GET] + requirements: + _role: "anonymous" os2loop_cura_login.authenticate: - path: '/os2loop-login-hack/authenticate' + path: "/os2loop-login-hack/authenticate" defaults: - _title: 'Authenticate with Cura login' + _title: "Authenticate with Cura login" _controller: '\Drupal\os2loop_cura_login\Controller\Os2loopCuraLoginController::authenticate' methods: [GET] requirements: - _role: 'anonymous' + _role: "anonymous" os2loop_cura_login.settings: - path: '/admin/config/os2loop/os2loop_cura_login/settings' + path: "/admin/config/os2loop/os2loop_cura_login/settings" defaults: _form: '\Drupal\os2loop_cura_login\Form\SettingsForm' - _title: 'OS2Loop Cura login settings' + _title: "OS2Loop Cura login settings" requirements: - _permission: 'administer site settings' + _permission: "administer site settings" diff --git a/web/profiles/custom/os2loop/modules/os2loop_cura_login/os2loop_cura_login.services.yml b/web/profiles/custom/os2loop/modules/os2loop_cura_login/os2loop_cura_login.services.yml index 3f350d095..a0b82e559 100644 --- a/web/profiles/custom/os2loop/modules/os2loop_cura_login/os2loop_cura_login.services.yml +++ b/web/profiles/custom/os2loop/modules/os2loop_cura_login/os2loop_cura_login.services.yml @@ -1,4 +1,4 @@ services: logger.channel.os2loop_cura_login: parent: logger.channel_base - arguments: ['os2loop_cura_login'] + arguments: ["os2loop_cura_login"] diff --git a/web/profiles/custom/os2loop/modules/os2loop_cura_login/src/Controller/Os2loopCuraLoginController.php b/web/profiles/custom/os2loop/modules/os2loop_cura_login/src/Controller/Os2loopCuraLoginController.php index 88fa8c89b..7c76f3db9 100644 --- a/web/profiles/custom/os2loop/modules/os2loop_cura_login/src/Controller/Os2loopCuraLoginController.php +++ b/web/profiles/custom/os2loop/modules/os2loop_cura_login/src/Controller/Os2loopCuraLoginController.php @@ -57,23 +57,29 @@ public function __construct( /** * Start user authentication. */ - public function start(Request $request): Response { + public function start(Request $request, ?string $jwt): Response { try { - $content = NULL; + $content = NULL; try { $content = (string) $request->getContent(); - } catch (\Exception) {} + } + catch (\Exception) { + } $this->debug('@debug', [ '@debug' => json_encode([ 'method' => $request->getMethod(), + 'headers' => $request->headers->all(), 'query' => $request->query->all(), 'content' => $content, ]), ]); - $jwt = Request::METHOD_POST === $request->getMethod() - ? $request->getContent() - : $request->query->getString($this->config->get('token_param_name') ?? 'token'); + if (empty($jwt)) { + $name = $this->config->get('payload_name') ?? 'payload'; + $jwt = Request::METHOD_POST === $request->getMethod() + ? $request->request->getString($name) + : $request->query->getString($name); + } $this->debug('@debug', [ '@debug' => json_encode([ @@ -85,7 +91,17 @@ public function start(Request $request): Response { throw new BadRequestHttpException('Missing or empty JWT'); } - $payload = (array) JWT::decode($jwt, new Key($this->config->get('signing_secret'), $this->config->get('signing_algorithm'))); + $secret = $this->config->get('signing_secret'); + // @todo Get rid of the double base64 encoding. + $secret = base64_decode($secret); + + $originalLeeway = JWT::$leeway; + $leeway = (int) $this->config->get('jwt_leeway'); + if ($leeway > 0) { + JWT::$leeway = $leeway; + } + $payload = (array) JWT::decode($jwt, new Key($secret, $this->config->get('signing_algorithm'))); + JWT::$leeway = $originalLeeway; $this->debug('@debug', [ '@debug' => json_encode([ @@ -93,7 +109,7 @@ public function start(Request $request): Response { ]), ]); - $username = $payload['username'] ?? NULL; + $username = $payload['username'] ?? $payload['brugerId'] ?? NULL; if (empty($username)) { throw new BadRequestHttpException('Missing username'); } @@ -221,9 +237,11 @@ private function getUserinfo(User $user): array { ]; } - public function log($level, \Stringable|string $message, array $context = []): void - { - // Lifted from LoggerChannel + /** + * {@inheritdoc} + */ + public function log($level, \Stringable|string $message, array $context = []): void { + // Lifted from LoggerChannel. $levels = [ LogLevel::EMERGENCY => RfcLogLevel::EMERGENCY, LogLevel::ALERT => RfcLogLevel::ALERT, @@ -235,7 +253,7 @@ public function log($level, \Stringable|string $message, array $context = []): v LogLevel::DEBUG => RfcLogLevel::DEBUG, ]; $rfcLogLevel = $levels[$level] ?? RfcLogLevel::ERROR; - if ((int)$this->config->get('log_level') >= $rfcLogLevel) { + if ((int) $this->config->get('log_level') >= $rfcLogLevel) { $this->logger->log($level, $message, $context); } } diff --git a/web/profiles/custom/os2loop/modules/os2loop_cura_login/src/Drush/Commands/Os2loopCuraLoginCommands.php b/web/profiles/custom/os2loop/modules/os2loop_cura_login/src/Drush/Commands/Os2loopCuraLoginCommands.php index f4d49c7b6..a7f5153e5 100644 --- a/web/profiles/custom/os2loop/modules/os2loop_cura_login/src/Drush/Commands/Os2loopCuraLoginCommands.php +++ b/web/profiles/custom/os2loop/modules/os2loop_cura_login/src/Drush/Commands/Os2loopCuraLoginCommands.php @@ -2,24 +2,19 @@ namespace Drupal\os2loop_cura_login\Drush\Commands; -use Consolidation\OutputFormatters\StructuredData\RowsOfFields; use Drupal\Component\Datetime\TimeInterface; use Drupal\Core\DependencyInjection\AutowireTrait; use Drupal\Core\Url; -use Drupal\Core\Utility\Token; use Drush\Attributes as CLI; use Drush\Commands\DrushCommands; use Firebase\JWT\JWT; -use Http\Client\HttpClient; use Psr\Http\Client\ClientInterface; -use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\HttpFoundation\Request; /** * A Drush commandfile. */ -final class Os2loopCuraLoginCommands extends DrushCommands -{ +final class Os2loopCuraLoginCommands extends DrushCommands { use AutowireTrait; /** @@ -42,10 +37,10 @@ public function __construct( public function commandName( $username, $options = [ - 'get' => null, - 'secret' => null, + 'get' => NULL, + 'secret' => NULL, 'algorithm' => 'HS256', - ] + ], ) { // https://github.com/firebase/php-jwt?tab=readme-ov-file#example $payload = [ @@ -62,11 +57,12 @@ public function commandName( if ($name = $options['get']) { $method = Request::METHOD_GET; $routeParameters[$name] = $jwt; - } else { + } + else { $method = Request::METHOD_POST; $requestOptions['body'] = $jwt; } - $url = Url::fromRoute('os2loop_cura_login.start', $routeParameters)->setAbsolute()->toString(true)->getGeneratedUrl(); + $url = Url::fromRoute('os2loop_cura_login.start', $routeParameters)->setAbsolute()->toString(TRUE)->getGeneratedUrl(); $this->io()->writeln($method === Request::METHOD_POST ? sprintf('POST\'ing to %s', $url) : sprintf('GET\'ing %s', $url), @@ -78,7 +74,7 @@ public function commandName( $url, $request->getStatusCode(), $request->getBody()->getContents(), - ], true); + ], TRUE); die(__FILE__ . ':' . __LINE__ . ':' . __METHOD__); } diff --git a/web/profiles/custom/os2loop/modules/os2loop_cura_login/src/Form/SettingsForm.php b/web/profiles/custom/os2loop/modules/os2loop_cura_login/src/Form/SettingsForm.php index cbb2e469f..2ffdc2447 100644 --- a/web/profiles/custom/os2loop/modules/os2loop_cura_login/src/Form/SettingsForm.php +++ b/web/profiles/custom/os2loop/modules/os2loop_cura_login/src/Form/SettingsForm.php @@ -66,11 +66,17 @@ public function buildForm(array $form, FormStateInterface $form_state): array { '#description' => $this->t('Check this to generate a new random secret'), ]; - $form['token_param_name'] = [ + $form['payload_name'] = [ '#type' => 'textfield', - '#title' => $this->t('Token param name'), - '#default_value' => $config->get('token_param_name'), - '#description' => $this->t('Query string param name used for JWT token on GET request'), + '#title' => $this->t('Payload name'), + '#default_value' => $config->get('payload_name') ?? 'payload', + '#description' => $this->t('Name of parameter used for payload'), + ]; + + $form['jwt_leeway'] = [ + '#type' => 'textfield', + '#title' => $this->t('JWT leeway'), + '#default_value' => $config->get('jwt_leeway') ?? 0, ]; $form['log_level'] = [ @@ -80,42 +86,31 @@ public function buildForm(array $form, FormStateInterface $form_state): array { '#default_value' => $config->get('log_level') ?? RfcLogLevel::ERROR, ]; - $authenticationStartUrl = Url::fromRoute('os2loop_cura_login.start')->setAbsolute()->toString(true)->getGeneratedUrl(); + $authenticationStartUrl = Url::fromRoute('os2loop_cura_login.start')->setAbsolute()->toString(TRUE)->getGeneratedUrl(); $form['info'] = [ '#theme' => 'item_list', '#items' => [ - '#markup' => $this->t('Use :url as linkURL.', [':url' => $authenticationStartUrl]), + '#markup' => $this->t('Use :url as linkURL for postToGetLinkURL ≡ true.', [':url' => $authenticationStartUrl]), ], ]; - if ($name = $config->get('token_param_name')) { - $authenticationStartUrl = Url::fromRoute('os2loop_cura_login.start', [$name => '…'])->setAbsolute()->toString(true)->getGeneratedUrl(); - $authenticationStartUrl = str_replace(urlencode('…'), '…', $authenticationStartUrl); + $authenticationStartUrl = Url::fromRoute('os2loop_cura_login.start', [])->setAbsolute()->toString(TRUE)->getGeneratedUrl(); + $authenticationStartUrl = rtrim($authenticationStartUrl, '/') . '/'; + $form['info']['#items'][] = [ + '#markup' => $this->t('Use :url as linkURL for postToGetLinkURL ≡ false.', [':url' => $authenticationStartUrl]), + ]; + + if ($name = $config->get('payload_name')) { + $authenticationStartUrl = Url::fromRoute('os2loop_cura_login.start', [$name => '…'])->setAbsolute()->toString(TRUE)->getGeneratedUrl(); + $authenticationStartUrl = str_replace(urlencode('…'), '', $authenticationStartUrl); $form['info']['#items'][] = [ - '#markup' => $this->t('Use :url as linkURL for GET.', [':url' => $authenticationStartUrl]), + '#markup' => $this->t('Use :url as linkURL for postToGetLinkURL ≡ false.', [':url' => $authenticationStartUrl]), ]; } return parent::buildForm($form, $form_state); } - /** - * {@inheritdoc} - */ - public function validateForm(array &$form, FormStateInterface $form_state): void { - // @todo Validate the form here. - // Example: - // @code - // if ($form_state->getValue('example') === 'wrong') { - // $form_state->setErrorByName( - // 'message', - // $this->t('The value is not correct.'), - // ); - // } - // @endcode - parent::validateForm($form, $form_state); - } - /** * {@inheritdoc} */ @@ -127,7 +122,8 @@ public function submitForm(array &$form, FormStateInterface $form_state): void { $this->config('os2loop_cura_login.settings') ->set('signing_algorithm', $form_state->getValue('signing_algorithm')) ->set('signing_secret', $secret) - ->set('token_param_name', $form_state->getValue('token_param_name')) + ->set('payload_name', $form_state->getValue('payload_name')) + ->set('jwt_leeway', $form_state->getValue('jwt_leeway')) ->set('log_level', $form_state->getValue('log_level')) ->save(); parent::submitForm($form, $form_state); From cdcf3cade43f7ac6390b95fe2638468cf07e273b Mon Sep 17 00:00:00 2001 From: Mikkel Ricky Date: Thu, 4 Sep 2025 16:11:32 +0200 Subject: [PATCH 07/15] 5124: Refactored settings --- .../os2loop_cura_login.services.yml | 5 + .../Controller/Os2loopCuraLoginController.php | 19 ++-- .../Commands/Os2loopCuraLoginCommands.php | 5 +- .../src/Form/SettingsForm.php | 48 +++++---- .../os2loop_cura_login/src/Settings.php | 101 ++++++++++++++++++ 5 files changed, 142 insertions(+), 36 deletions(-) create mode 100644 web/profiles/custom/os2loop/modules/os2loop_cura_login/src/Settings.php diff --git a/web/profiles/custom/os2loop/modules/os2loop_cura_login/os2loop_cura_login.services.yml b/web/profiles/custom/os2loop/modules/os2loop_cura_login/os2loop_cura_login.services.yml index a0b82e559..add5a5b5c 100644 --- a/web/profiles/custom/os2loop/modules/os2loop_cura_login/os2loop_cura_login.services.yml +++ b/web/profiles/custom/os2loop/modules/os2loop_cura_login/os2loop_cura_login.services.yml @@ -1,4 +1,9 @@ services: + _defaults: + autowire: true + logger.channel.os2loop_cura_login: parent: logger.channel_base arguments: ["os2loop_cura_login"] + + Drupal\os2loop_cura_login\Settings: diff --git a/web/profiles/custom/os2loop/modules/os2loop_cura_login/src/Controller/Os2loopCuraLoginController.php b/web/profiles/custom/os2loop/modules/os2loop_cura_login/src/Controller/Os2loopCuraLoginController.php index 7c76f3db9..61c231582 100644 --- a/web/profiles/custom/os2loop/modules/os2loop_cura_login/src/Controller/Os2loopCuraLoginController.php +++ b/web/profiles/custom/os2loop/modules/os2loop_cura_login/src/Controller/Os2loopCuraLoginController.php @@ -5,11 +5,11 @@ namespace Drupal\os2loop_cura_login\Controller; use Drupal\Component\Datetime\TimeInterface; -use Drupal\Core\Config\ImmutableConfig; use Drupal\Core\Controller\ControllerBase; use Drupal\Core\Logger\RfcLogLevel; use Drupal\Core\Routing\TrustedRedirectResponse; use Drupal\Core\Url; +use Drupal\os2loop_cura_login\Settings; use Drupal\user\Entity\User; use Drupal\user\UserStorageInterface; use Firebase\JWT\JWT; @@ -37,21 +37,16 @@ final class Os2loopCuraLoginController extends ControllerBase { */ private readonly UserStorageInterface $userStorage; - /** - * The module config. - */ - private readonly ImmutableConfig $config; - /** * Constructor. */ public function __construct( + private readonly Settings $settings, private readonly TimeInterface $time, #[Autowire(service: 'logger.channel.os2loop_cura_login')] private readonly LoggerInterface $logger, ) { $this->userStorage = $this->entityTypeManager()->getStorage('user'); - $this->config = $this->config('os2loop_cura_login.settings'); } /** @@ -75,7 +70,7 @@ public function start(Request $request, ?string $jwt): Response { ]); if (empty($jwt)) { - $name = $this->config->get('payload_name') ?? 'payload'; + $name = $this->settings->getPayloadName(); $jwt = Request::METHOD_POST === $request->getMethod() ? $request->request->getString($name) : $request->query->getString($name); @@ -91,16 +86,16 @@ public function start(Request $request, ?string $jwt): Response { throw new BadRequestHttpException('Missing or empty JWT'); } - $secret = $this->config->get('signing_secret'); + $secret = $this->settings->getSigningSecret(); // @todo Get rid of the double base64 encoding. $secret = base64_decode($secret); $originalLeeway = JWT::$leeway; - $leeway = (int) $this->config->get('jwt_leeway'); + $leeway = $this->settings->getJwtLeeway(); if ($leeway > 0) { JWT::$leeway = $leeway; } - $payload = (array) JWT::decode($jwt, new Key($secret, $this->config->get('signing_algorithm'))); + $payload = (array) JWT::decode($jwt, new Key($secret, $this->settings->getSigningAlgorithm())); JWT::$leeway = $originalLeeway; $this->debug('@debug', [ @@ -253,7 +248,7 @@ public function log($level, \Stringable|string $message, array $context = []): v LogLevel::DEBUG => RfcLogLevel::DEBUG, ]; $rfcLogLevel = $levels[$level] ?? RfcLogLevel::ERROR; - if ((int) $this->config->get('log_level') >= $rfcLogLevel) { + if ((int) $this->settings->getLogLevel() >= $rfcLogLevel) { $this->logger->log($level, $message, $context); } } diff --git a/web/profiles/custom/os2loop/modules/os2loop_cura_login/src/Drush/Commands/Os2loopCuraLoginCommands.php b/web/profiles/custom/os2loop/modules/os2loop_cura_login/src/Drush/Commands/Os2loopCuraLoginCommands.php index a7f5153e5..61bf38a2b 100644 --- a/web/profiles/custom/os2loop/modules/os2loop_cura_login/src/Drush/Commands/Os2loopCuraLoginCommands.php +++ b/web/profiles/custom/os2loop/modules/os2loop_cura_login/src/Drush/Commands/Os2loopCuraLoginCommands.php @@ -34,9 +34,10 @@ public function __construct( #[CLI\Argument(name: 'username', description: 'The username.')] #[CLI\Option(name: 'post', description: 'Use POST to get the login URL')] #[CLI\Usage(name: 'os2loop-cura-login:get-login-url test@example.com', description: 'Get login URL')] - public function commandName( + public function getLoginUrl( $username, $options = [ + 'linkUrl' => NULL, 'get' => NULL, 'secret' => NULL, 'algorithm' => 'HS256', @@ -60,7 +61,7 @@ public function commandName( } else { $method = Request::METHOD_POST; - $requestOptions['body'] = $jwt; + $requestOptions['body'] = ['payload' => $jwt]; } $url = Url::fromRoute('os2loop_cura_login.start', $routeParameters)->setAbsolute()->toString(TRUE)->getGeneratedUrl(); $this->io()->writeln($method === Request::METHOD_POST diff --git a/web/profiles/custom/os2loop/modules/os2loop_cura_login/src/Form/SettingsForm.php b/web/profiles/custom/os2loop/modules/os2loop_cura_login/src/Form/SettingsForm.php index 2ffdc2447..691712ef2 100644 --- a/web/profiles/custom/os2loop/modules/os2loop_cura_login/src/Form/SettingsForm.php +++ b/web/profiles/custom/os2loop/modules/os2loop_cura_login/src/Form/SettingsForm.php @@ -5,15 +5,26 @@ namespace Drupal\os2loop_cura_login\Form; use Drupal\Component\Utility\Random; +use Drupal\Core\Config\ConfigFactoryInterface; +use Drupal\Core\DependencyInjection\AutowireTrait; use Drupal\Core\Form\ConfigFormBase; use Drupal\Core\Form\FormStateInterface; use Drupal\Core\Logger\RfcLogLevel; use Drupal\Core\Url; +use Drupal\os2loop_cura_login\Settings; /** * Configure OS2Loop Cura login settings for this site. */ final class SettingsForm extends ConfigFormBase { + use AutowireTrait; + + public function __construct( + ConfigFactoryInterface $config_factory, + private readonly Settings $settings, + ) { + parent::__construct($config_factory); + } /** * {@inheritdoc} @@ -26,34 +37,30 @@ public function getFormId(): string { * {@inheritdoc} */ protected function getEditableConfigNames(): array { - return ['os2loop_cura_login.settings']; + return [Settings::CONFIG_NAME]; } /** * {@inheritdoc} */ public function buildForm(array $form, FormStateInterface $form_state): array { - $config = $this->config('os2loop_cura_login.settings'); + $settings = \Drupal::service(Settings::class); $form['signing_algorithm'] = [ '#required' => TRUE, '#type' => 'select', - '#options' => [ - 'HS256' => 'HS256', - 'HS384' => 'HS384', - 'HS512' => 'HS512', - ], + '#options' => Settings::SIGNING_ALGORITHMS, '#title' => $this->t('Signing algorithm'), - '#default_value' => $config->get('signing_algorithm'), + '#default_value' => $this->settings->getSigningAlgorithm(), ]; - $hasSigningSecret = !empty($config->get('signing_secret')); + $hasSigningSecret = !empty($this->settings->getSigningSecret()); $form['signing_secret'] = [ '#type' => 'textfield', '#size' => 128, '#required' => $hasSigningSecret, '#title' => $this->t('Signing secret'), - '#default_value' => $config->get('signing_secret'), + '#default_value' => $this->settings->getSigningSecret(), '#description' => !$hasSigningSecret ? $this->t('Save the configuration to generate a random secret.') : '', @@ -69,21 +76,21 @@ public function buildForm(array $form, FormStateInterface $form_state): array { $form['payload_name'] = [ '#type' => 'textfield', '#title' => $this->t('Payload name'), - '#default_value' => $config->get('payload_name') ?? 'payload', + '#default_value' => $this->settings->getPayloadName(), '#description' => $this->t('Name of parameter used for payload'), ]; $form['jwt_leeway'] = [ '#type' => 'textfield', '#title' => $this->t('JWT leeway'), - '#default_value' => $config->get('jwt_leeway') ?? 0, + '#default_value' => $this->settings->getJwtLeeway(), ]; $form['log_level'] = [ '#type' => 'select', '#options' => RfcLogLevel::getLevels(), '#title' => $this->t('Log level'), - '#default_value' => $config->get('log_level') ?? RfcLogLevel::ERROR, + '#default_value' => $this->settings->getLogLevel(), ]; $authenticationStartUrl = Url::fromRoute('os2loop_cura_login.start')->setAbsolute()->toString(TRUE)->getGeneratedUrl(); @@ -100,7 +107,7 @@ public function buildForm(array $form, FormStateInterface $form_state): array { '#markup' => $this->t('Use :url as linkURL for postToGetLinkURL ≡ false.', [':url' => $authenticationStartUrl]), ]; - if ($name = $config->get('payload_name')) { + if ($name = $this->settings->getPayloadName()) { $authenticationStartUrl = Url::fromRoute('os2loop_cura_login.start', [$name => '…'])->setAbsolute()->toString(TRUE)->getGeneratedUrl(); $authenticationStartUrl = str_replace(urlencode('…'), '', $authenticationStartUrl); $form['info']['#items'][] = [ @@ -117,15 +124,12 @@ public function buildForm(array $form, FormStateInterface $form_state): array { public function submitForm(array &$form, FormStateInterface $form_state): void { $secret = $form_state->getValue('signing_secret'); if ($form_state->getValue('generate_new_secret')) { - $secret = base64_encode((new Random())->string(64)); + $form_state->setValue('signing_secret', base64_encode((new Random())->string(64))); } - $this->config('os2loop_cura_login.settings') - ->set('signing_algorithm', $form_state->getValue('signing_algorithm')) - ->set('signing_secret', $secret) - ->set('payload_name', $form_state->getValue('payload_name')) - ->set('jwt_leeway', $form_state->getValue('jwt_leeway')) - ->set('log_level', $form_state->getValue('log_level')) - ->save(); + /** @var \Drupal\os2loop_cura_login\Settings $settings */ + $settings = \Drupal::service(Settings::class); + $settings->saveSettings($form_state); + parent::submitForm($form, $form_state); } diff --git a/web/profiles/custom/os2loop/modules/os2loop_cura_login/src/Settings.php b/web/profiles/custom/os2loop/modules/os2loop_cura_login/src/Settings.php new file mode 100644 index 000000000..a4bf84b1a --- /dev/null +++ b/web/profiles/custom/os2loop/modules/os2loop_cura_login/src/Settings.php @@ -0,0 +1,101 @@ + 'HS256', + 'HS384' => 'HS384', + 'HS512' => 'HS512', + ]; + + /** + * The config. + */ + private readonly ImmutableConfig $config; + + /** + * Constructor. + */ + public function __construct( + private readonly ConfigFactoryInterface $configFactory, + ) { + $this->config = $configFactory->get(self::CONFIG_NAME); + } + + /** + * Get payload name. + */ + public function getPayloadName(): string { + return $this->config->get('payload_name') ?? 'payload'; + } + + /** + * Get signing algorithm. + */ + public function getSigningAlgorithm(): string { + return $this->config->get('signing_algorithm') ?? self::SIGNING_ALGORITHMS[array_key_first(self::SIGNING_ALGORITHMS)]; + } + + /** + * Get signing secret. + */ + public function getSigningSecret(): string { + return $this->config->get('signing_secret') ?? ''; + } + + /** + * Get JWT leeway. + */ + public function getJwtLeeway(): int { + return (int) $this->config->get('jwt_leeway'); + } + + /** + * Get log level. + */ + public function getLogLevel() { + return (int) $this->config->get('log_level') ?? RfcLogLevel::ERROR; + } + + /** + * Save settings. + */ + public function saveSettings(array|FormStateInterface $values): array { + if ($values instanceof FormStateInterface) { + $values = [ + self::SETTING_SIGNING_ALGORITHM => $values->getValue(self::SETTING_SIGNING_ALGORITHM), + self::SETTING_SIGNING_SECRET => $values->getValue(self::SETTING_SIGNING_SECRET), + self::SETTING_PAYLOAD_NAME => $values->getValue(self::SETTING_PAYLOAD_NAME), + self::SETTING_JWT_LEEWAY => $values->getValue(self::SETTING_JWT_LEEWAY), + self::SETTING_LOG_LEVEL => $values->getValue(self::SETTING_LOG_LEVEL), + ]; + } + + // @todo validate values + $config = $this->configFactory->getEditable(self::CONFIG_NAME); + foreach ($values as $key => $value) { + $config->set($key, $value); + } + $config->save(); + + return $config->get(); + } + +} From b57261b13d663370b1f2e9f8127232704930b20e Mon Sep 17 00:00:00 2001 From: Mikkel Ricky Date: Thu, 4 Sep 2025 22:06:52 +0200 Subject: [PATCH 08/15] 5124: Updated controller actions --- .../os2loop_cura_login.routing.yml | 2 +- .../Controller/Os2loopCuraLoginController.php | 203 ++++++++++++------ 2 files changed, 134 insertions(+), 71 deletions(-) diff --git a/web/profiles/custom/os2loop/modules/os2loop_cura_login/os2loop_cura_login.routing.yml b/web/profiles/custom/os2loop/modules/os2loop_cura_login/os2loop_cura_login.routing.yml index 825034a2d..877506e7e 100644 --- a/web/profiles/custom/os2loop/modules/os2loop_cura_login/os2loop_cura_login.routing.yml +++ b/web/profiles/custom/os2loop/modules/os2loop_cura_login/os2loop_cura_login.routing.yml @@ -17,7 +17,7 @@ os2loop_cura_login.start_get_jwt: _role: "anonymous" os2loop_cura_login.authenticate: - path: "/os2loop-login-hack/authenticate" + path: "/os2loop-cura-login/authenticate" defaults: _title: "Authenticate with Cura login" _controller: '\Drupal\os2loop_cura_login\Controller\Os2loopCuraLoginController::authenticate' diff --git a/web/profiles/custom/os2loop/modules/os2loop_cura_login/src/Controller/Os2loopCuraLoginController.php b/web/profiles/custom/os2loop/modules/os2loop_cura_login/src/Controller/Os2loopCuraLoginController.php index 61c231582..81d751671 100644 --- a/web/profiles/custom/os2loop/modules/os2loop_cura_login/src/Controller/Os2loopCuraLoginController.php +++ b/web/profiles/custom/os2loop/modules/os2loop_cura_login/src/Controller/Os2loopCuraLoginController.php @@ -7,10 +7,9 @@ use Drupal\Component\Datetime\TimeInterface; use Drupal\Core\Controller\ControllerBase; use Drupal\Core\Logger\RfcLogLevel; -use Drupal\Core\Routing\TrustedRedirectResponse; use Drupal\Core\Url; use Drupal\os2loop_cura_login\Settings; -use Drupal\user\Entity\User; +use Drupal\user\UserInterface; use Drupal\user\UserStorageInterface; use Firebase\JWT\JWT; use Firebase\JWT\Key; @@ -19,7 +18,6 @@ use Psr\Log\LogLevel; use Symfony\Component\DependencyInjection\Attribute\Autowire; use Symfony\Component\HttpFoundation\Exception\BadRequestException; -use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\Exception\BadRequestHttpException; @@ -86,17 +84,7 @@ public function start(Request $request, ?string $jwt): Response { throw new BadRequestHttpException('Missing or empty JWT'); } - $secret = $this->settings->getSigningSecret(); - // @todo Get rid of the double base64 encoding. - $secret = base64_decode($secret); - - $originalLeeway = JWT::$leeway; - $leeway = $this->settings->getJwtLeeway(); - if ($leeway > 0) { - JWT::$leeway = $leeway; - } - $payload = (array) JWT::decode($jwt, new Key($secret, $this->settings->getSigningAlgorithm())); - JWT::$leeway = $originalLeeway; + $payload = $this->decodeJwt($jwt); $this->debug('@debug', [ '@debug' => json_encode([ @@ -109,51 +97,36 @@ public function start(Request $request, ?string $jwt): Response { throw new BadRequestHttpException('Missing username'); } - $user = $this->loadUser($username); + // Check that we can get userinfo. + $userinfo = $this->fetchUserinfo($username); $this->debug('@debug', [ '@debug' => json_encode([ - 'user' => $user, + 'userinfo' => $userinfo, ]), ]); - if (empty($user)) { + if (empty($userinfo)) { // Don't disclose whether or not the user exists. throw new BadRequestHttpException(); } - // Check that we can get userinfo. - $userinfo = $this->getUserinfo($user); + $user = $this->ensureUser($username, $userinfo); $this->debug('@debug', [ '@debug' => json_encode([ - 'userinfo' => $userinfo, + 'user' => $user, ]), ]); - if (empty($userinfo)) { + if (empty($user)) { + // Don't disclose whether or not the user exists. throw new BadRequestHttpException(); } - // https://github.com/firebase/php-jwt?tab=readme-ov-file#example - $payload = [ - // Issued at. - 'iat' => $this->time->getRequestTime(), - // Expire af 60 seconds. - 'exp' => $this->time->getRequestTime() + 60, - 'username' => $username, - ]; - $jwt = JWT::encode($payload, self::JWT_KEY, 'HS256'); - - $url = Url::fromRoute('os2loop_cura_login.authenticate', [ - 'username' => $username, - 'jwt' => $jwt, - ])->setAbsolute()->toString(TRUE)->getGeneratedUrl(); - - return new JsonResponse([ - 'authenticate_url' => $url, - 'jwt' => $jwt, - ]); + return Request::METHOD_POST === $request->getMethod() + ? $this->createAuthenticateResponse($user) + : $this->authenticateUser($user); } catch (\Exception $exception) { $this->error('start: @message', ['@message' => $exception->getMessage(), $exception]); @@ -162,17 +135,37 @@ public function start(Request $request, ?string $jwt): Response { } /** - * Authenticate user. + * Create authenticate response. + */ + private function createAuthenticateResponse(UserInterface $user): Response { + // https://github.com/firebase/php-jwt?tab=readme-ov-file#example + $payload = [ + // Issued at. + 'iat' => $this->time->getRequestTime(), + // Expire af 60 seconds. + 'exp' => $this->time->getRequestTime() + 60, + 'username' => $user->getAccountName(), + ]; + $jwt = $this->encodeJwt($payload); + + $url = Url::fromRoute('os2loop_cura_login.authenticate', [ + 'jwt' => $jwt, + ])->setAbsolute()->toString(TRUE)->getGeneratedUrl(); + + return new Response($url); + } + + /** + * Authenticate action. */ public function authenticate(Request $request): Response { try { - $username = $request->get('username'); $jwt = $request->get('jwt'); - if (empty($username) || empty($jwt)) { + if (empty($jwt)) { throw new BadRequestHttpException(); } - $payload = (array) JWT::decode($jwt, new Key(self::JWT_KEY, 'HS256')); + $payload = $this->decodeJwt($jwt); $username = $payload['username'] ?? NULL; if (empty($username)) { throw new BadRequestHttpException(); @@ -184,54 +177,124 @@ public function authenticate(Request $request): Response { throw new BadRequestHttpException(); } - $this->updateUser($user); - - user_login_finalize($user); - - $url = Url::fromRoute('')->setAbsolute()->toString(TRUE)->getGeneratedUrl(); - $this->messenger()->addStatus($this->t('Welcome @user.', ['@user' => $user->getDisplayName()])); - - return new TrustedRedirectResponse($url); + return $this->authenticateUser($user); } catch (\Exception $exception) { - $this->error('start: @message', ['@message' => $exception->getMessage(), $exception]); + $this->error('authenticate: @message', ['@message' => $exception->getMessage(), $exception]); throw new BadRequestException($exception->getMessage()); } } /** - * Load user by username. - * - * @param string $username - * The username. - * - * @return \Drupal\user\Entity\User|null - * The user if any. + * Authenticate user. */ - private function loadUser(string $username): ?User { - $users = $this->userStorage->loadByProperties(['name' => $username]); + private function authenticateUser($user): Response { + user_login_finalize($user); - return reset($users) ?: NULL; + $this->messenger()->addStatus($this->t('Welcome Cura user @user.', ['@user' => $user->getDisplayName()])); + + return $this->redirect(''); } /** - * Update user with info from IdP. + * Encode JWT. */ - private function updateUser(User $user): User { - // $userinfo = $this->getUserinfo($user); - // @todo Update user. - return $user; + private function encodeJwt(array $payload): string { + $secret = $this->settings->getSigningSecret(); + // @todo Get rid of the double base64 encoding. + $secret = base64_decode($secret); + + return JWT::encode($payload, $secret, $this->settings->getSigningAlgorithm()); + } + + /** + * Decode JWT. + */ + private function decodeJwt(string $jwt): array { + $secret = $this->settings->getSigningSecret(); + // @todo Get rid of the double base64 encoding. + $secret = base64_decode($secret); + + $originalLeeway = JWT::$leeway; + $leeway = $this->settings->getJwtLeeway(); + if ($leeway > 0) { + JWT::$leeway = $leeway; + } + $payload = (array) JWT::decode($jwt, new Key($secret, $this->settings->getSigningAlgorithm())); + JWT::$leeway = $originalLeeway; + + return $payload; } /** * Get user info from userinfo endpoint. */ - private function getUserinfo(User $user): array { + private function fetchUserinfo(string $username): array { return [ - 'name' => $user->getDisplayName(), + // Drupal user fields. + 'name' => $username, + 'mail' => $username . '@cura.example.com', + + // OS2Lloop fields + // 'os2loop_user_address' => '', + // 'os2loop_user_areas_of_expertise' => '', + // 'os2loop_user_biography' => '', + // 'os2loop_user_city' => '', + // 'os2loop_user_external_list' => '',. + 'os2loop_user_family_name' => 'Cura', + 'os2loop_user_given_name' => 'User', + // 'os2loop_user_image' => '', + // 'os2loop_user_internal_list' => '', + // 'os2loop_user_job_title' => '', + // 'os2loop_user_phone_number' => '', + // 'os2loop_user_place' => '', + // 'os2loop_user_postal_code' => '', + // 'os2loop_user_professions' => '', ]; } + /** + * Ensure user exists. + * + * @param string $username + * The username. + * @param array $userinfo + * The user info to set on the user. + * + * @return \Drupal\user\Entity\UserInterface + * The newly created or updated user. + */ + private function ensureUser(string $username, array $userinfo): UserInterface { + $user = $this->loadUser($username); + + if (NULL === $user) { + $user = $this->userStorage->create(); + } + + foreach ($userinfo as $field => $value) { + $currentValue = $user->get($field); + if ($currentValue !== $value) { + $user->set($field, $value); + } + } + + // Make sure that the user is active. + $user + ->activate() + ->save(); + + return $user; + } + + /** + * Load user by username. + */ + private function loadUser(string $username) : ?UserInterface { + $users = $this->userStorage->loadByProperties(['name' => $username]); + + return reset($users) ?: NULL; + } + /** * {@inheritdoc} */ From 221fb5b80c7a33f8e945b0126d7ef194dc4f0dfa Mon Sep 17 00:00:00 2001 From: Mikkel Ricky Date: Fri, 5 Sep 2025 12:25:01 +0200 Subject: [PATCH 09/15] 5124: Refactored settings --- .../modules/os2loop_cura_login/README.md | 2 +- .../os2loop_cura_login.routing.yml | 2 +- .../os2loop_cura_login.services.yml | 6 + .../Controller/Os2loopCuraLoginController.php | 172 ++++-------------- .../os2loop_cura_login/src/CuraHelper.php | 41 +++++ .../src/Form/SettingsForm.php | 141 ++++++++++---- .../os2loop_cura_login/src/IdPHelper.php | 53 ++++++ .../os2loop_cura_login/src/Settings.php | 73 ++++---- .../os2loop_cura_login/src/Settings/Cura.php | 55 ++++++ .../os2loop_cura_login/src/Settings/IdP.php | 28 +++ .../os2loop_cura_login/src/UserHelper.php | 66 +++++++ 11 files changed, 421 insertions(+), 218 deletions(-) create mode 100644 web/profiles/custom/os2loop/modules/os2loop_cura_login/src/CuraHelper.php create mode 100644 web/profiles/custom/os2loop/modules/os2loop_cura_login/src/IdPHelper.php create mode 100644 web/profiles/custom/os2loop/modules/os2loop_cura_login/src/Settings/Cura.php create mode 100644 web/profiles/custom/os2loop/modules/os2loop_cura_login/src/Settings/IdP.php create mode 100644 web/profiles/custom/os2loop/modules/os2loop_cura_login/src/UserHelper.php diff --git a/web/profiles/custom/os2loop/modules/os2loop_cura_login/README.md b/web/profiles/custom/os2loop/modules/os2loop_cura_login/README.md index 37ef9c098..945add927 100644 --- a/web/profiles/custom/os2loop/modules/os2loop_cura_login/README.md +++ b/web/profiles/custom/os2loop/modules/os2loop_cura_login/README.md @@ -45,7 +45,7 @@ drush --uri='http://nginx:8080' os2loop-cura-login:get-login-url test@example.co ``` php # settings.local.php -$config['os2loop_cura_login.settings']['log_level'] = \Drupal\Core\Logger\RfcLogLevel::DEBUG; +$config['os2loop_cura_login.settings']['general']['log_level'] = \Drupal\Core\Logger\RfcLogLevel::DEBUG; ``` Run diff --git a/web/profiles/custom/os2loop/modules/os2loop_cura_login/os2loop_cura_login.routing.yml b/web/profiles/custom/os2loop/modules/os2loop_cura_login/os2loop_cura_login.routing.yml index 877506e7e..3214da259 100644 --- a/web/profiles/custom/os2loop/modules/os2loop_cura_login/os2loop_cura_login.routing.yml +++ b/web/profiles/custom/os2loop/modules/os2loop_cura_login/os2loop_cura_login.routing.yml @@ -17,7 +17,7 @@ os2loop_cura_login.start_get_jwt: _role: "anonymous" os2loop_cura_login.authenticate: - path: "/os2loop-cura-login/authenticate" + path: "/os2loop-cura-login/authenticate/{jwt}" defaults: _title: "Authenticate with Cura login" _controller: '\Drupal\os2loop_cura_login\Controller\Os2loopCuraLoginController::authenticate' diff --git a/web/profiles/custom/os2loop/modules/os2loop_cura_login/os2loop_cura_login.services.yml b/web/profiles/custom/os2loop/modules/os2loop_cura_login/os2loop_cura_login.services.yml index add5a5b5c..6d2ee44dd 100644 --- a/web/profiles/custom/os2loop/modules/os2loop_cura_login/os2loop_cura_login.services.yml +++ b/web/profiles/custom/os2loop/modules/os2loop_cura_login/os2loop_cura_login.services.yml @@ -7,3 +7,9 @@ services: arguments: ["os2loop_cura_login"] Drupal\os2loop_cura_login\Settings: + + Drupal\os2loop_cura_login\CuraHelper: + + Drupal\os2loop_cura_login\IdPHelper: + + Drupal\os2loop_cura_login\UserHelper: diff --git a/web/profiles/custom/os2loop/modules/os2loop_cura_login/src/Controller/Os2loopCuraLoginController.php b/web/profiles/custom/os2loop/modules/os2loop_cura_login/src/Controller/Os2loopCuraLoginController.php index 81d751671..9f26b3f2a 100644 --- a/web/profiles/custom/os2loop/modules/os2loop_cura_login/src/Controller/Os2loopCuraLoginController.php +++ b/web/profiles/custom/os2loop/modules/os2loop_cura_login/src/Controller/Os2loopCuraLoginController.php @@ -8,11 +8,12 @@ use Drupal\Core\Controller\ControllerBase; use Drupal\Core\Logger\RfcLogLevel; use Drupal\Core\Url; +use Drupal\os2loop_cura_login\CuraHelper; +use Drupal\os2loop_cura_login\IdPHelper; use Drupal\os2loop_cura_login\Settings; +use Drupal\os2loop_cura_login\UserHelper; use Drupal\user\UserInterface; -use Drupal\user\UserStorageInterface; use Firebase\JWT\JWT; -use Firebase\JWT\Key; use Psr\Log\LoggerInterface; use Psr\Log\LoggerTrait; use Psr\Log\LogLevel; @@ -30,21 +31,18 @@ final class Os2loopCuraLoginController extends ControllerBase { private const JWT_KEY = 'os2loop_cura_login'; - /** - * The user storage. - */ - private readonly UserStorageInterface $userStorage; - /** * Constructor. */ public function __construct( + private readonly CuraHelper $curaHelper, + private readonly IdPHelper $idPHelper, + private readonly UserHelper $userHelper, private readonly Settings $settings, private readonly TimeInterface $time, #[Autowire(service: 'logger.channel.os2loop_cura_login')] private readonly LoggerInterface $logger, ) { - $this->userStorage = $this->entityTypeManager()->getStorage('user'); } /** @@ -68,7 +66,7 @@ public function start(Request $request, ?string $jwt): Response { ]); if (empty($jwt)) { - $name = $this->settings->getPayloadName(); + $name = $this->settings->getCuraSettings()->getPayloadName(); $jwt = Request::METHOD_POST === $request->getMethod() ? $request->request->getString($name) : $request->query->getString($name); @@ -84,21 +82,14 @@ public function start(Request $request, ?string $jwt): Response { throw new BadRequestHttpException('Missing or empty JWT'); } - $payload = $this->decodeJwt($jwt); - - $this->debug('@debug', [ - '@debug' => json_encode([ - 'payload' => $payload, - ]), - ]); + [$payload, $username] = $this->curaHelper->decodeJwt($jwt); - $username = $payload['username'] ?? $payload['brugerId'] ?? NULL; if (empty($username)) { throw new BadRequestHttpException('Missing username'); } // Check that we can get userinfo. - $userinfo = $this->fetchUserinfo($username); + $userinfo = $this->idPHelper->fetchUserInfo($username); $this->debug('@debug', [ '@debug' => json_encode([ @@ -111,7 +102,7 @@ public function start(Request $request, ?string $jwt): Response { throw new BadRequestHttpException(); } - $user = $this->ensureUser($username, $userinfo); + $user = $this->userHelper->ensureUser($username, $userinfo); $this->debug('@debug', [ '@debug' => json_encode([ @@ -134,44 +125,22 @@ public function start(Request $request, ?string $jwt): Response { } } - /** - * Create authenticate response. - */ - private function createAuthenticateResponse(UserInterface $user): Response { - // https://github.com/firebase/php-jwt?tab=readme-ov-file#example - $payload = [ - // Issued at. - 'iat' => $this->time->getRequestTime(), - // Expire af 60 seconds. - 'exp' => $this->time->getRequestTime() + 60, - 'username' => $user->getAccountName(), - ]; - $jwt = $this->encodeJwt($payload); - - $url = Url::fromRoute('os2loop_cura_login.authenticate', [ - 'jwt' => $jwt, - ])->setAbsolute()->toString(TRUE)->getGeneratedUrl(); - - return new Response($url); - } - /** * Authenticate action. */ - public function authenticate(Request $request): Response { + public function authenticate(Request $request, string $jwt): Response { try { - $jwt = $request->get('jwt'); if (empty($jwt)) { throw new BadRequestHttpException(); } - $payload = $this->decodeJwt($jwt); + [$payload, $_] = $this->curaHelper->decodeJwt($jwt); $username = $payload['username'] ?? NULL; if (empty($username)) { throw new BadRequestHttpException(); } - $user = $this->loadUser($username); + $user = $this->userHelper->loadUser($username); if (empty($user)) { // Don't disclose whether or not the user exists. throw new BadRequestHttpException(); @@ -185,6 +154,27 @@ public function authenticate(Request $request): Response { } } + /** + * Create authenticate response. + */ + private function createAuthenticateResponse(UserInterface $user): Response { + // https://github.com/firebase/php-jwt?tab=readme-ov-file#example + $payload = [ + // Issued at. + 'iat' => $this->time->getRequestTime(), + // Expire af 60 seconds. + 'exp' => $this->time->getRequestTime() + 60, + 'username' => $user->getAccountName(), + ]; + $jwt = $this->encodeJwt($payload); + + $url = Url::fromRoute('os2loop_cura_login.authenticate', [ + 'jwt' => $jwt, + ])->setAbsolute()->toString(TRUE)->getGeneratedUrl(); + + return new Response($url); + } + /** * Authenticate user. */ @@ -200,99 +190,9 @@ private function authenticateUser($user): Response { * Encode JWT. */ private function encodeJwt(array $payload): string { - $secret = $this->settings->getSigningSecret(); - // @todo Get rid of the double base64 encoding. - $secret = base64_decode($secret); - - return JWT::encode($payload, $secret, $this->settings->getSigningAlgorithm()); - } - - /** - * Decode JWT. - */ - private function decodeJwt(string $jwt): array { - $secret = $this->settings->getSigningSecret(); - // @todo Get rid of the double base64 encoding. - $secret = base64_decode($secret); - - $originalLeeway = JWT::$leeway; - $leeway = $this->settings->getJwtLeeway(); - if ($leeway > 0) { - JWT::$leeway = $leeway; - } - $payload = (array) JWT::decode($jwt, new Key($secret, $this->settings->getSigningAlgorithm())); - JWT::$leeway = $originalLeeway; - - return $payload; - } - - /** - * Get user info from userinfo endpoint. - */ - private function fetchUserinfo(string $username): array { - return [ - // Drupal user fields. - 'name' => $username, - 'mail' => $username . '@cura.example.com', - - // OS2Lloop fields - // 'os2loop_user_address' => '', - // 'os2loop_user_areas_of_expertise' => '', - // 'os2loop_user_biography' => '', - // 'os2loop_user_city' => '', - // 'os2loop_user_external_list' => '',. - 'os2loop_user_family_name' => 'Cura', - 'os2loop_user_given_name' => 'User', - // 'os2loop_user_image' => '', - // 'os2loop_user_internal_list' => '', - // 'os2loop_user_job_title' => '', - // 'os2loop_user_phone_number' => '', - // 'os2loop_user_place' => '', - // 'os2loop_user_postal_code' => '', - // 'os2loop_user_professions' => '', - ]; - } - - /** - * Ensure user exists. - * - * @param string $username - * The username. - * @param array $userinfo - * The user info to set on the user. - * - * @return \Drupal\user\Entity\UserInterface - * The newly created or updated user. - */ - private function ensureUser(string $username, array $userinfo): UserInterface { - $user = $this->loadUser($username); - - if (NULL === $user) { - $user = $this->userStorage->create(); - } - - foreach ($userinfo as $field => $value) { - $currentValue = $user->get($field); - if ($currentValue !== $value) { - $user->set($field, $value); - } - } - - // Make sure that the user is active. - $user - ->activate() - ->save(); - - return $user; - } - - /** - * Load user by username. - */ - private function loadUser(string $username) : ?UserInterface { - $users = $this->userStorage->loadByProperties(['name' => $username]); + $settings = $this->settings->getCuraSettings(); - return reset($users) ?: NULL; + return JWT::encode($payload, $settings->getSigningSecret(), $settings->getSigningAlgorithm()); } /** diff --git a/web/profiles/custom/os2loop/modules/os2loop_cura_login/src/CuraHelper.php b/web/profiles/custom/os2loop/modules/os2loop_cura_login/src/CuraHelper.php new file mode 100644 index 000000000..acdaf3de5 --- /dev/null +++ b/web/profiles/custom/os2loop/modules/os2loop_cura_login/src/CuraHelper.php @@ -0,0 +1,41 @@ +settings = $settings->getCuraSettings(); + } + + /** + * Decode JWT. + */ + public function decodeJwt(string $jwt): array { + $secret = $this->settings->getSigningSecret(); + + $originalLeeway = JWT::$leeway; + $leeway = $this->settings->getJwtLeeway(); + if ($leeway > 0) { + JWT::$leeway = $leeway; + } + $payload = (array) JWT::decode($jwt, new Key($secret, $this->settings->getSigningAlgorithm())); + JWT::$leeway = $originalLeeway; + + return [$payload, $payload['username'] ?? $payload['brugerId'] ?? NULL]; + } + +} diff --git a/web/profiles/custom/os2loop/modules/os2loop_cura_login/src/Form/SettingsForm.php b/web/profiles/custom/os2loop/modules/os2loop_cura_login/src/Form/SettingsForm.php index 691712ef2..4cece00a2 100644 --- a/web/profiles/custom/os2loop/modules/os2loop_cura_login/src/Form/SettingsForm.php +++ b/web/profiles/custom/os2loop/modules/os2loop_cura_login/src/Form/SettingsForm.php @@ -10,8 +10,11 @@ use Drupal\Core\Form\ConfigFormBase; use Drupal\Core\Form\FormStateInterface; use Drupal\Core\Logger\RfcLogLevel; +use Drupal\Core\Messenger\MessengerInterface; use Drupal\Core\Url; use Drupal\os2loop_cura_login\Settings; +use Drupal\os2loop_cura_login\Settings\Cura; +use Drupal\os2loop_cura_login\Settings\IdP; /** * Configure OS2Loop Cura login settings for this site. @@ -19,6 +22,8 @@ final class SettingsForm extends ConfigFormBase { use AutowireTrait; + private const string GENERATE_NEW_SECRET = 'generate_new_secret'; + public function __construct( ConfigFactoryInterface $config_factory, private readonly Settings $settings, @@ -44,91 +49,147 @@ protected function getEditableConfigNames(): array { * {@inheritdoc} */ public function buildForm(array $form, FormStateInterface $form_state): array { - $settings = \Drupal::service(Settings::class); - $form['signing_algorithm'] = [ + $form[Cura::NAME] = [ + '#tree' => TRUE, + '#type' => 'fieldset', + '#title' => t('Cura'), + ] + $this->buildCuraForm($form_state); + + $form[IdP::NAME] = [ + '#tree' => TRUE, + '#type' => 'fieldset', + '#title' => t('IdP'), + ] + $this->buildIdPForm($form_state); + + $form[Settings::NAME] = [ + '#tree' => TRUE, + '#type' => 'fieldset', + '#title' => t('General settings'), + + Settings::SETTING_LOG_LEVEL => [ + '#type' => 'select', + '#options' => RfcLogLevel::getLevels(), + '#title' => $this->t('Log level'), + '#default_value' => $this->settings->getLogLevel(), + ], + ]; + + return parent::buildForm($form, $form_state); + } + + /** + * Build Cura form. + */ + public function buildCuraForm(FormStateInterface $form_state): array { + $settings = $this->settings->getCuraSettings(); + $form[Cura::SETTING_SIGNING_ALGORITHM] = [ '#required' => TRUE, '#type' => 'select', - '#options' => Settings::SIGNING_ALGORITHMS, + '#options' => Cura::SIGNING_ALGORITHMS, '#title' => $this->t('Signing algorithm'), - '#default_value' => $this->settings->getSigningAlgorithm(), + '#default_value' => $settings->getSigningAlgorithm(), ]; - $hasSigningSecret = !empty($this->settings->getSigningSecret()); + $hasSigningSecret = !empty($settings->getSigningSecret()); - $form['signing_secret'] = [ + $form[Cura::SETTING_SIGNING_SECRET] = [ '#type' => 'textfield', '#size' => 128, '#required' => $hasSigningSecret, '#title' => $this->t('Signing secret'), - '#default_value' => $this->settings->getSigningSecret(), + '#default_value' => $settings->getSigningSecret(), '#description' => !$hasSigningSecret ? $this->t('Save the configuration to generate a random secret.') : '', ]; - $form['generate_new_secret'] = [ + $form[self::GENERATE_NEW_SECRET] = [ '#title' => $this->t('Generate new secret'), '#type' => 'checkbox', '#default_value' => !$hasSigningSecret, '#description' => $this->t('Check this to generate a new random secret'), ]; - $form['payload_name'] = [ + $form[Cura::SETTING_PAYLOAD_NAME] = [ '#type' => 'textfield', '#title' => $this->t('Payload name'), - '#default_value' => $this->settings->getPayloadName(), + '#default_value' => $settings->getPayloadName(), '#description' => $this->t('Name of parameter used for payload'), ]; - $form['jwt_leeway'] = [ + $form[Cura::SETTING_JWT_LEEWAY] = [ '#type' => 'textfield', '#title' => $this->t('JWT leeway'), - '#default_value' => $this->settings->getJwtLeeway(), + '#default_value' => $settings->getJwtLeeway(), ]; - $form['log_level'] = [ - '#type' => 'select', - '#options' => RfcLogLevel::getLevels(), - '#title' => $this->t('Log level'), - '#default_value' => $this->settings->getLogLevel(), - ]; + $authenticationStartUrlPost = Url::fromRoute('os2loop_cura_login.start')->setAbsolute()->toString(TRUE)->getGeneratedUrl(); + $authenticationStartUrlGet = rtrim($authenticationStartUrlPost, '/') . '/'; - $authenticationStartUrl = Url::fromRoute('os2loop_cura_login.start')->setAbsolute()->toString(TRUE)->getGeneratedUrl(); $form['info'] = [ - '#theme' => 'item_list', - '#items' => [ - '#markup' => $this->t('Use :url as linkURL for postToGetLinkURL ≡ true.', [':url' => $authenticationStartUrl]), + '#type' => 'details', + '#title' => $this->t('Cura link settings'), + '#description' => $this->t('Use these settings to set up a link in Cura (cf. the documentation we cannot refer to)'), + 'info' => [ + '#markup' => $this->t(' +
+
signingAlgorithm
+
:signing_algorithm
+ +
signingSecret (Base64 encoded)
+
:signing_secret
+ +
linkURL (postToGetLinkURL = false)
+
:link_url_get
+ +
linkURL (postToGetLinkURL = true)
+
:link_url_post
+
+', + [ + ':signing_algorithm' => $settings->getSigningAlgorithm(), + ':signing_secret' => base64_encode($settings->getSigningSecret()), + ':link_url_get' => $authenticationStartUrlGet, + ':link_url_post' => $authenticationStartUrlPost, + ] + ), ], ]; - $authenticationStartUrl = Url::fromRoute('os2loop_cura_login.start', [])->setAbsolute()->toString(TRUE)->getGeneratedUrl(); - $authenticationStartUrl = rtrim($authenticationStartUrl, '/') . '/'; - $form['info']['#items'][] = [ - '#markup' => $this->t('Use :url as linkURL for postToGetLinkURL ≡ false.', [':url' => $authenticationStartUrl]), + return $form; + } + + /** + * Build IdP form. + */ + public function buildIdpForm(FormStateInterface $form_state): array { + $settings = $this->settings->getIdpSettings(); + + $form['todo'] = [ + '#theme' => 'status_messages', + '#message_list' => [ + MessengerInterface::TYPE_WARNING => [$this->t('This section is incomplete.')], + ], ]; - if ($name = $this->settings->getPayloadName()) { - $authenticationStartUrl = Url::fromRoute('os2loop_cura_login.start', [$name => '…'])->setAbsolute()->toString(TRUE)->getGeneratedUrl(); - $authenticationStartUrl = str_replace(urlencode('…'), '', $authenticationStartUrl); - $form['info']['#items'][] = [ - '#markup' => $this->t('Use :url as linkURL for postToGetLinkURL ≡ false.', [':url' => $authenticationStartUrl]), - ]; - } + $form[IdP::USERNAME_CLAIM] = [ + '#type' => 'textfield', + '#required' => TRUE, + '#title' => $this->t('Username claim'), + '#default_value' => $settings->getUsernameClaim(), + ]; - return parent::buildForm($form, $form_state); + return $form; } /** * {@inheritdoc} */ public function submitForm(array &$form, FormStateInterface $form_state): void { - $secret = $form_state->getValue('signing_secret'); - if ($form_state->getValue('generate_new_secret')) { - $form_state->setValue('signing_secret', base64_encode((new Random())->string(64))); + if ($form_state->getValue([Cura::NAME, self::GENERATE_NEW_SECRET])) { + $form_state->setValue([Cura::NAME, Cura::SETTING_SIGNING_SECRET], (new Random())->name(64)); } - /** @var \Drupal\os2loop_cura_login\Settings $settings */ - $settings = \Drupal::service(Settings::class); - $settings->saveSettings($form_state); + $this->settings->saveSettings($form_state); parent::submitForm($form, $form_state); } diff --git a/web/profiles/custom/os2loop/modules/os2loop_cura_login/src/IdPHelper.php b/web/profiles/custom/os2loop/modules/os2loop_cura_login/src/IdPHelper.php new file mode 100644 index 000000000..2588b5c91 --- /dev/null +++ b/web/profiles/custom/os2loop/modules/os2loop_cura_login/src/IdPHelper.php @@ -0,0 +1,53 @@ +settings = $settings->getIdpSettings(); + } + + /** + * Get user info from userinfo endpoint. + */ + public function fetchUserinfo(string $username): array { + $query = [ + $this->settings->getUsernameClaim() => $username, + ]; + // $result = fetch($query) + return [ + // Drupal user fields. + 'name' => $username, + 'mail' => $username . '@cura.example.com', + + // OS2Lloop fields + // 'os2loop_user_address' => '', + // 'os2loop_user_areas_of_expertise' => '', + // 'os2loop_user_biography' => '', + // 'os2loop_user_city' => '', + // 'os2loop_user_external_list' => '',. + 'os2loop_user_family_name' => 'Cura', + 'os2loop_user_given_name' => 'User', + // 'os2loop_user_image' => '', + // 'os2loop_user_internal_list' => '', + // 'os2loop_user_job_title' => '', + // 'os2loop_user_phone_number' => '', + // 'os2loop_user_place' => '', + // 'os2loop_user_postal_code' => '', + // 'os2loop_user_professions' => '', + ]; + } + +} diff --git a/web/profiles/custom/os2loop/modules/os2loop_cura_login/src/Settings.php b/web/profiles/custom/os2loop/modules/os2loop_cura_login/src/Settings.php index a4bf84b1a..f8f86f72f 100644 --- a/web/profiles/custom/os2loop/modules/os2loop_cura_login/src/Settings.php +++ b/web/profiles/custom/os2loop/modules/os2loop_cura_login/src/Settings.php @@ -6,6 +6,8 @@ use Drupal\Core\Config\ImmutableConfig; use Drupal\Core\Form\FormStateInterface; use Drupal\Core\Logger\RfcLogLevel; +use Drupal\os2loop_cura_login\Settings\Cura; +use Drupal\os2loop_cura_login\Settings\IdP; /** * Settings for OS2Loop Cura login. @@ -13,23 +15,24 @@ final class Settings { const string CONFIG_NAME = 'os2loop_cura_login.settings'; - private const SETTING_SIGNING_SECRET = 'signing_secret'; - private const SETTING_SIGNING_ALGORITHM = 'signing_algorithm'; - private const SETTING_PAYLOAD_NAME = 'payload_name'; - private const SETTING_JWT_LEEWAY = 'jwt_leeway'; - private const SETTING_LOG_LEVEL = 'log_level'; - - const array SIGNING_ALGORITHMS = [ - 'HS256' => 'HS256', - 'HS384' => 'HS384', - 'HS512' => 'HS512', - ]; + const string NAME = 'general'; + const string SETTING_LOG_LEVEL = 'log_level'; /** * The config. */ private readonly ImmutableConfig $config; + /** + * The Cura settings. + */ + private readonly Cura $curaSettings; + + /** + * The IdP settings. + */ + private readonly IdP $idpSettings; + /** * Constructor. */ @@ -40,53 +43,43 @@ public function __construct( } /** - * Get payload name. + * Get Cura settings. */ - public function getPayloadName(): string { - return $this->config->get('payload_name') ?? 'payload'; - } + public function getCuraSettings() { + if (!isset($this->curaSettings)) { + $this->curaSettings = new Cura($this->config->get(Cura::NAME) ?? []); + } - /** - * Get signing algorithm. - */ - public function getSigningAlgorithm(): string { - return $this->config->get('signing_algorithm') ?? self::SIGNING_ALGORITHMS[array_key_first(self::SIGNING_ALGORITHMS)]; + return $this->curaSettings; } /** - * Get signing secret. + * Get IdP settings. */ - public function getSigningSecret(): string { - return $this->config->get('signing_secret') ?? ''; - } + public function getIdpSettings() { + if (!isset($this->idpSettings)) { + $this->idpSettings = new IdP($this->config->get(IdP::NAME) ?? []); + } - /** - * Get JWT leeway. - */ - public function getJwtLeeway(): int { - return (int) $this->config->get('jwt_leeway'); + return $this->idpSettings; } /** * Get log level. */ public function getLogLevel() { - return (int) $this->config->get('log_level') ?? RfcLogLevel::ERROR; + return (int) $this->config->get(self::SETTING_LOG_LEVEL) ?? RfcLogLevel::ERROR; } /** * Save settings. */ - public function saveSettings(array|FormStateInterface $values): array { - if ($values instanceof FormStateInterface) { - $values = [ - self::SETTING_SIGNING_ALGORITHM => $values->getValue(self::SETTING_SIGNING_ALGORITHM), - self::SETTING_SIGNING_SECRET => $values->getValue(self::SETTING_SIGNING_SECRET), - self::SETTING_PAYLOAD_NAME => $values->getValue(self::SETTING_PAYLOAD_NAME), - self::SETTING_JWT_LEEWAY => $values->getValue(self::SETTING_JWT_LEEWAY), - self::SETTING_LOG_LEVEL => $values->getValue(self::SETTING_LOG_LEVEL), - ]; - } + public function saveSettings(FormStateInterface $formState): array { + $values = array_filter( + $formState->getValues(), + static fn(string $key) => in_array($key, [self::NAME, Cura::NAME, IdP::NAME], TRUE), + ARRAY_FILTER_USE_KEY + ); // @todo validate values $config = $this->configFactory->getEditable(self::CONFIG_NAME); diff --git a/web/profiles/custom/os2loop/modules/os2loop_cura_login/src/Settings/Cura.php b/web/profiles/custom/os2loop/modules/os2loop_cura_login/src/Settings/Cura.php new file mode 100644 index 000000000..20b7d92cf --- /dev/null +++ b/web/profiles/custom/os2loop/modules/os2loop_cura_login/src/Settings/Cura.php @@ -0,0 +1,55 @@ + 'HS256', + 'HS384' => 'HS384', + 'HS512' => 'HS512', + ]; + + public function __construct( + private readonly array $config, + ) { + } + + /** + * Get payload name. + */ + public function getPayloadName(): string { + return $this->config[self::SETTING_PAYLOAD_NAME] ?? 'payload'; + } + + /** + * Get signing algorithm. + */ + public function getSigningAlgorithm(): string { + return $this->config['signing_algorithm'] ?? self::SIGNING_ALGORITHMS[array_key_first(self::SIGNING_ALGORITHMS)]; + } + + /** + * Get signing secret. + */ + public function getSigningSecret(): string { + return $this->config['signing_secret'] ?? ''; + } + + /** + * Get JWT leeway. + */ + public function getJwtLeeway(): int { + return (int) ($this->config['jwt_leeway'] ?? 0); + } + +} diff --git a/web/profiles/custom/os2loop/modules/os2loop_cura_login/src/Settings/IdP.php b/web/profiles/custom/os2loop/modules/os2loop_cura_login/src/Settings/IdP.php new file mode 100644 index 000000000..da8843bd3 --- /dev/null +++ b/web/profiles/custom/os2loop/modules/os2loop_cura_login/src/Settings/IdP.php @@ -0,0 +1,28 @@ +config[self::USERNAME_CLAIM] ?? 'upn'; + } + +} diff --git a/web/profiles/custom/os2loop/modules/os2loop_cura_login/src/UserHelper.php b/web/profiles/custom/os2loop/modules/os2loop_cura_login/src/UserHelper.php new file mode 100644 index 000000000..4cd38ed56 --- /dev/null +++ b/web/profiles/custom/os2loop/modules/os2loop_cura_login/src/UserHelper.php @@ -0,0 +1,66 @@ +userStorage = $entityTypeManager->getStorage('user'); + } + + /** + * Ensure user exists. + * + * @param string $username + * The username. + * @param array $userinfo + * The user info to set on the user. + * + * @return \Drupal\user\Entity\UserInterface + * The newly created or updated user. + */ + public function ensureUser(string $username, array $userinfo): UserInterface { + $user = $this->loadUser($username); + + if (NULL === $user) { + $user = $this->userStorage->create(); + } + + foreach ($userinfo as $field => $value) { + $currentValue = $user->get($field); + if ($currentValue !== $value) { + $user->set($field, $value); + } + } + + // Make sure that the user is active. + $user + ->activate() + ->save(); + + return $user; + } + + /** + * Load user by username. + */ + public function loadUser(string $username) : ?UserInterface { + $users = $this->userStorage->loadByProperties(['name' => $username]); + + return reset($users) ?: NULL; + } + +} From 479a495be5988e9e4258352c08c2e76d735456ef Mon Sep 17 00:00:00 2001 From: Mikkel Ricky Date: Fri, 5 Sep 2025 13:10:17 +0200 Subject: [PATCH 10/15] 5124: Updated Drush test command --- .../modules/os2loop_cura_login/README.md | 6 +++-- .../Commands/Os2loopCuraLoginCommands.php | 25 +++++++++++-------- 2 files changed, 18 insertions(+), 13 deletions(-) diff --git a/web/profiles/custom/os2loop/modules/os2loop_cura_login/README.md b/web/profiles/custom/os2loop/modules/os2loop_cura_login/README.md index 945add927..6770f2b8c 100644 --- a/web/profiles/custom/os2loop/modules/os2loop_cura_login/README.md +++ b/web/profiles/custom/os2loop/modules/os2loop_cura_login/README.md @@ -37,8 +37,10 @@ curl "http://$(docker compose port nginx 8080)/os2loop-cura-login/start" drush os2loop-cura-login:get-login-url --help ``` -``` shell -drush --uri='http://nginx:8080' os2loop-cura-login:get-login-url test@example.com --secret=$(drush config:get --format string os2loop_cura_login.settings signing_secret --include-overridden) --algorithm=$(drush config:get --format string os2loop_cura_login.settings signing_algorithm --include-overridden) +``` shell name=drush-get-login-url +drush --uri='http://nginx:8080' os2loop-cura-login:get-login-url test@example.com \ + --algorithm=$(drush config:get --format string os2loop_cura_login.settings cura.signing_algorithm --include-overridden) \ + --secret=$(drush config:get --format string os2loop_cura_login.settings cura.signing_secret --include-overridden) ``` ## Development and debugging diff --git a/web/profiles/custom/os2loop/modules/os2loop_cura_login/src/Drush/Commands/Os2loopCuraLoginCommands.php b/web/profiles/custom/os2loop/modules/os2loop_cura_login/src/Drush/Commands/Os2loopCuraLoginCommands.php index 61bf38a2b..d568465e0 100644 --- a/web/profiles/custom/os2loop/modules/os2loop_cura_login/src/Drush/Commands/Os2loopCuraLoginCommands.php +++ b/web/profiles/custom/os2loop/modules/os2loop_cura_login/src/Drush/Commands/Os2loopCuraLoginCommands.php @@ -4,6 +4,7 @@ use Drupal\Component\Datetime\TimeInterface; use Drupal\Core\DependencyInjection\AutowireTrait; +use Drupal\Core\Serialization\Yaml; use Drupal\Core\Url; use Drush\Attributes as CLI; use Drush\Commands\DrushCommands; @@ -12,7 +13,7 @@ use Symfony\Component\HttpFoundation\Request; /** - * A Drush commandfile. + * A Drush command file. */ final class Os2loopCuraLoginCommands extends DrushCommands { use AutowireTrait; @@ -53,30 +54,32 @@ public function getLoginUrl( ]; $jwt = JWT::encode($payload, $options['secret'], $options['algorithm']); + $routeName = 'os2loop_cura_login.start'; $routeParameters = []; $requestOptions = []; if ($name = $options['get']) { $method = Request::METHOD_GET; $routeParameters[$name] = $jwt; + if ('jwt' === $name) { + $routeName = 'os2loop_cura_login.start_get_jwt'; + } } else { $method = Request::METHOD_POST; - $requestOptions['body'] = ['payload' => $jwt]; + $requestOptions['form_params'] = ['payload' => $jwt]; } - $url = Url::fromRoute('os2loop_cura_login.start', $routeParameters)->setAbsolute()->toString(TRUE)->getGeneratedUrl(); + $url = Url::fromRoute($routeName, $routeParameters)->setAbsolute()->toString(TRUE)->getGeneratedUrl(); $this->io()->writeln($method === Request::METHOD_POST ? sprintf('POST\'ing to %s', $url) : sprintf('GET\'ing %s', $url), ); - $request = $this->httpClient->request($method, $url, $requestOptions); + $response = $this->httpClient->request($method, $url, $requestOptions); - header('content-type: text/plain'); - echo var_export([ - $url, - $request->getStatusCode(), - $request->getBody()->getContents(), - ], TRUE); - die(__FILE__ . ':' . __LINE__ . ':' . __METHOD__); + $this->io()->writeln(Yaml::encode([ + 'status' => $response->getStatusCode(), + 'headers' => Yaml::encode($response->getHeaders()), + 'body' => $response->getBody()->getContents(), + ])); } } From 6cea4eaf8f019a67ea263d9c49adc2ade87f6e5c Mon Sep 17 00:00:00 2001 From: Mikkel Ricky Date: Mon, 8 Sep 2025 12:52:14 +0200 Subject: [PATCH 11/15] 5124: Disabled caching on routes --- .../os2loop_cura_login.routing.yml | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/web/profiles/custom/os2loop/modules/os2loop_cura_login/os2loop_cura_login.routing.yml b/web/profiles/custom/os2loop/modules/os2loop_cura_login/os2loop_cura_login.routing.yml index 3214da259..b2d4e5473 100644 --- a/web/profiles/custom/os2loop/modules/os2loop_cura_login/os2loop_cura_login.routing.yml +++ b/web/profiles/custom/os2loop/modules/os2loop_cura_login/os2loop_cura_login.routing.yml @@ -5,7 +5,9 @@ os2loop_cura_login.start: _controller: '\Drupal\os2loop_cura_login\Controller\Os2loopCuraLoginController::start' methods: [GET, POST] requirements: - _role: "anonymous" + _user_is_logged_in: "FALSE" + options: + no_cache: TRUE os2loop_cura_login.start_get_jwt: path: "/os2loop-cura-login/start/{jwt}" @@ -14,7 +16,9 @@ os2loop_cura_login.start_get_jwt: _controller: '\Drupal\os2loop_cura_login\Controller\Os2loopCuraLoginController::start' methods: [GET] requirements: - _role: "anonymous" + _user_is_logged_in: "FALSE" + options: + no_cache: TRUE os2loop_cura_login.authenticate: path: "/os2loop-cura-login/authenticate/{jwt}" @@ -23,7 +27,9 @@ os2loop_cura_login.authenticate: _controller: '\Drupal\os2loop_cura_login\Controller\Os2loopCuraLoginController::authenticate' methods: [GET] requirements: - _role: "anonymous" + _user_is_logged_in: "FALSE" + options: + no_cache: TRUE os2loop_cura_login.settings: path: "/admin/config/os2loop/os2loop_cura_login/settings" From c3e2ad423dba31743c9a9338f7b654cce94709d8 Mon Sep 17 00:00:00 2001 From: Mikkel Ricky Date: Mon, 8 Sep 2025 12:55:10 +0200 Subject: [PATCH 12/15] 5124: Handled `destination` parameter in URL --- .../Controller/Os2loopCuraLoginController.php | 34 ++++++++++++++----- .../os2loop_cura_login/src/UserHelper.php | 7 ++++ 2 files changed, 32 insertions(+), 9 deletions(-) diff --git a/web/profiles/custom/os2loop/modules/os2loop_cura_login/src/Controller/Os2loopCuraLoginController.php b/web/profiles/custom/os2loop/modules/os2loop_cura_login/src/Controller/Os2loopCuraLoginController.php index 9f26b3f2a..014d74f59 100644 --- a/web/profiles/custom/os2loop/modules/os2loop_cura_login/src/Controller/Os2loopCuraLoginController.php +++ b/web/profiles/custom/os2loop/modules/os2loop_cura_login/src/Controller/Os2loopCuraLoginController.php @@ -19,6 +19,7 @@ use Psr\Log\LogLevel; use Symfony\Component\DependencyInjection\Attribute\Autowire; use Symfony\Component\HttpFoundation\Exception\BadRequestException; +use Symfony\Component\HttpFoundation\RedirectResponse; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\Exception\BadRequestHttpException; @@ -116,8 +117,8 @@ public function start(Request $request, ?string $jwt): Response { } return Request::METHOD_POST === $request->getMethod() - ? $this->createAuthenticateResponse($user) - : $this->authenticateUser($user); + ? $this->createAuthenticateResponse($user, $request) + : $this->authenticateUser($user, $request); } catch (\Exception $exception) { $this->error('start: @message', ['@message' => $exception->getMessage(), $exception]); @@ -146,7 +147,7 @@ public function authenticate(Request $request, string $jwt): Response { throw new BadRequestHttpException(); } - return $this->authenticateUser($user); + return $this->authenticateUser($user, $request); } catch (\Exception $exception) { $this->error('authenticate: @message', ['@message' => $exception->getMessage(), $exception]); @@ -157,7 +158,7 @@ public function authenticate(Request $request, string $jwt): Response { /** * Create authenticate response. */ - private function createAuthenticateResponse(UserInterface $user): Response { + private function createAuthenticateResponse(UserInterface $user, Request $request): Response { // https://github.com/firebase/php-jwt?tab=readme-ov-file#example $payload = [ // Issued at. @@ -168,9 +169,15 @@ private function createAuthenticateResponse(UserInterface $user): Response { ]; $jwt = $this->encodeJwt($payload); - $url = Url::fromRoute('os2loop_cura_login.authenticate', [ + $routeParameters = [ 'jwt' => $jwt, - ])->setAbsolute()->toString(TRUE)->getGeneratedUrl(); + ]; + if ($destination = $request->query->get('destination')) { + $routeParameters['destination'] = $destination; + } + + $url = Url::fromRoute('os2loop_cura_login.authenticate', $routeParameters) + ->setAbsolute()->toString(TRUE)->getGeneratedUrl(); return new Response($url); } @@ -178,12 +185,21 @@ private function createAuthenticateResponse(UserInterface $user): Response { /** * Authenticate user. */ - private function authenticateUser($user): Response { - user_login_finalize($user); + private function authenticateUser($user, Request $request): Response { + $this->userHelper->authenticateUser($user); $this->messenger()->addStatus($this->t('Welcome Cura user @user.', ['@user' => $user->getDisplayName()])); + $url = Url::fromRoute(''); + if ($destination = $request->query->get('destination')) { + try { + $url = Url::fromUserInput($destination); + } + catch (\Exception) { + // Ignore any exceptions. + } + } - return $this->redirect(''); + return new RedirectResponse($url->setAbsolute()->toString()); } /** diff --git a/web/profiles/custom/os2loop/modules/os2loop_cura_login/src/UserHelper.php b/web/profiles/custom/os2loop/modules/os2loop_cura_login/src/UserHelper.php index 4cd38ed56..b4e4a6744 100644 --- a/web/profiles/custom/os2loop/modules/os2loop_cura_login/src/UserHelper.php +++ b/web/profiles/custom/os2loop/modules/os2loop_cura_login/src/UserHelper.php @@ -63,4 +63,11 @@ public function loadUser(string $username) : ?UserInterface { return reset($users) ?: NULL; } + /** + * Authenticate user. + */ + public function authenticateUser($user) { + user_login_finalize($user); + } + } From f267d5f76ef0bf79601a0f4e23d146342a23d3b7 Mon Sep 17 00:00:00 2001 From: Mikkel Ricky Date: Mon, 8 Sep 2025 12:55:23 +0200 Subject: [PATCH 13/15] 5124: Updated test command --- .../modules/os2loop_cura_login/README.md | 6 ++-- .../Commands/Os2loopCuraLoginCommands.php | 30 ++++++++++++------- 2 files changed, 23 insertions(+), 13 deletions(-) diff --git a/web/profiles/custom/os2loop/modules/os2loop_cura_login/README.md b/web/profiles/custom/os2loop/modules/os2loop_cura_login/README.md index 6770f2b8c..b03ecaede 100644 --- a/web/profiles/custom/os2loop/modules/os2loop_cura_login/README.md +++ b/web/profiles/custom/os2loop/modules/os2loop_cura_login/README.md @@ -38,9 +38,9 @@ drush os2loop-cura-login:get-login-url --help ``` ``` shell name=drush-get-login-url -drush --uri='http://nginx:8080' os2loop-cura-login:get-login-url test@example.com \ - --algorithm=$(drush config:get --format string os2loop_cura_login.settings cura.signing_algorithm --include-overridden) \ - --secret=$(drush config:get --format string os2loop_cura_login.settings cura.signing_secret --include-overridden) +drush os2loop-cura-login:get-login-url test@example.com --get=jwt --destination=/user \ + --algorithm="$(drush config:get --format string os2loop_cura_login.settings cura.signing_algorithm --include-overridden)" \ + --secret="$(drush config:get --format string os2loop_cura_login.settings cura.signing_secret --include-overridden)" ``` ## Development and debugging diff --git a/web/profiles/custom/os2loop/modules/os2loop_cura_login/src/Drush/Commands/Os2loopCuraLoginCommands.php b/web/profiles/custom/os2loop/modules/os2loop_cura_login/src/Drush/Commands/Os2loopCuraLoginCommands.php index d568465e0..269e3de48 100644 --- a/web/profiles/custom/os2loop/modules/os2loop_cura_login/src/Drush/Commands/Os2loopCuraLoginCommands.php +++ b/web/profiles/custom/os2loop/modules/os2loop_cura_login/src/Drush/Commands/Os2loopCuraLoginCommands.php @@ -42,6 +42,7 @@ public function getLoginUrl( 'get' => NULL, 'secret' => NULL, 'algorithm' => 'HS256', + 'destination' => NULL, ], ) { // https://github.com/firebase/php-jwt?tab=readme-ov-file#example @@ -56,6 +57,9 @@ public function getLoginUrl( $routeName = 'os2loop_cura_login.start'; $routeParameters = []; + if ($destination = trim($options['destination'])) { + $routeParameters['destination'] = $destination; + } $requestOptions = []; if ($name = $options['get']) { $method = Request::METHOD_GET; @@ -69,17 +73,23 @@ public function getLoginUrl( $requestOptions['form_params'] = ['payload' => $jwt]; } $url = Url::fromRoute($routeName, $routeParameters)->setAbsolute()->toString(TRUE)->getGeneratedUrl(); - $this->io()->writeln($method === Request::METHOD_POST - ? sprintf('POST\'ing to %s', $url) - : sprintf('GET\'ing %s', $url), - ); - $response = $this->httpClient->request($method, $url, $requestOptions); - $this->io()->writeln(Yaml::encode([ - 'status' => $response->getStatusCode(), - 'headers' => Yaml::encode($response->getHeaders()), - 'body' => $response->getBody()->getContents(), - ])); + if ($method === Request::METHOD_GET) { + $this->io()->writeln($url); + } + else { + $this->io()->writeln(sprintf('POST\'ing to %s', $url)); + $response = $this->httpClient->request($method, $url, $requestOptions); + + $contents = $response->getBody()->getContents(); + $this->io()->writeln(Yaml::encode([ + 'status' => $response->getStatusCode(), + 'headers' => Yaml::encode($response->getHeaders()), + 'contents' => $contents, + ])); + + $this->io()->writeln($contents); + } } } From 9010a2d058cdfad37a9393b9385daec501b1adf7 Mon Sep 17 00:00:00 2001 From: Mikkel Ricky Date: Mon, 8 Sep 2025 14:44:58 +0200 Subject: [PATCH 14/15] 5124: Added some logging --- .../modules/os2loop_cura_login/README.md | 2 +- .../os2loop_cura_login.info.yml | 1 + .../Controller/Os2loopCuraLoginController.php | 7 ++- .../os2loop_cura_login/src/CuraHelper.php | 3 + .../os2loop_cura_login/src/IdPHelper.php | 58 ++++++++++++------- .../os2loop_cura_login/src/Settings.php | 2 +- .../src/Trait/ControllerAwareTrait.php | 35 +++++++++++ .../os2loop_cura_login/src/UserHelper.php | 29 +++++++--- 8 files changed, 103 insertions(+), 34 deletions(-) create mode 100644 web/profiles/custom/os2loop/modules/os2loop_cura_login/src/Trait/ControllerAwareTrait.php diff --git a/web/profiles/custom/os2loop/modules/os2loop_cura_login/README.md b/web/profiles/custom/os2loop/modules/os2loop_cura_login/README.md index b03ecaede..da679cac8 100644 --- a/web/profiles/custom/os2loop/modules/os2loop_cura_login/README.md +++ b/web/profiles/custom/os2loop/modules/os2loop_cura_login/README.md @@ -38,7 +38,7 @@ drush os2loop-cura-login:get-login-url --help ``` ``` shell name=drush-get-login-url -drush os2loop-cura-login:get-login-url test@example.com --get=jwt --destination=/user \ +drush os2loop-cura-login:get-login-url az000000 --get=jwt --destination=/user \ --algorithm="$(drush config:get --format string os2loop_cura_login.settings cura.signing_algorithm --include-overridden)" \ --secret="$(drush config:get --format string os2loop_cura_login.settings cura.signing_secret --include-overridden)" ``` diff --git a/web/profiles/custom/os2loop/modules/os2loop_cura_login/os2loop_cura_login.info.yml b/web/profiles/custom/os2loop/modules/os2loop_cura_login/os2loop_cura_login.info.yml index 6914d3d54..fc2a48a2b 100644 --- a/web/profiles/custom/os2loop/modules/os2loop_cura_login/os2loop_cura_login.info.yml +++ b/web/profiles/custom/os2loop/modules/os2loop_cura_login/os2loop_cura_login.info.yml @@ -5,5 +5,6 @@ package: "OS2Loop" core_version_requirement: ^10 || ^11 dependencies: - drupal:user + - openid_connect:openid_connect configure: os2loop_cura_login.settings diff --git a/web/profiles/custom/os2loop/modules/os2loop_cura_login/src/Controller/Os2loopCuraLoginController.php b/web/profiles/custom/os2loop/modules/os2loop_cura_login/src/Controller/Os2loopCuraLoginController.php index 014d74f59..49eb9799a 100644 --- a/web/profiles/custom/os2loop/modules/os2loop_cura_login/src/Controller/Os2loopCuraLoginController.php +++ b/web/profiles/custom/os2loop/modules/os2loop_cura_login/src/Controller/Os2loopCuraLoginController.php @@ -44,6 +44,9 @@ public function __construct( #[Autowire(service: 'logger.channel.os2loop_cura_login')] private readonly LoggerInterface $logger, ) { + $this->curaHelper->setController($this); + $this->idPHelper->setController($this); + $this->userHelper->setController($this); } /** @@ -107,7 +110,7 @@ public function start(Request $request, ?string $jwt): Response { $this->debug('@debug', [ '@debug' => json_encode([ - 'user' => $user, + 'user' => $user->toArray(), ]), ]); @@ -227,7 +230,7 @@ public function log($level, \Stringable|string $message, array $context = []): v LogLevel::DEBUG => RfcLogLevel::DEBUG, ]; $rfcLogLevel = $levels[$level] ?? RfcLogLevel::ERROR; - if ((int) $this->settings->getLogLevel() >= $rfcLogLevel) { + if ($this->settings->getLogLevel() >= $rfcLogLevel) { $this->logger->log($level, $message, $context); } } diff --git a/web/profiles/custom/os2loop/modules/os2loop_cura_login/src/CuraHelper.php b/web/profiles/custom/os2loop/modules/os2loop_cura_login/src/CuraHelper.php index acdaf3de5..43b24df64 100644 --- a/web/profiles/custom/os2loop/modules/os2loop_cura_login/src/CuraHelper.php +++ b/web/profiles/custom/os2loop/modules/os2loop_cura_login/src/CuraHelper.php @@ -3,6 +3,7 @@ namespace Drupal\os2loop_cura_login; use Drupal\os2loop_cura_login\Settings\Cura; +use Drupal\os2loop_cura_login\Trait\ControllerAwareTrait; use Firebase\JWT\JWT; use Firebase\JWT\Key; @@ -10,6 +11,8 @@ * Cura helper. */ final class CuraHelper { + use ControllerAwareTrait; + /** * The settings. */ diff --git a/web/profiles/custom/os2loop/modules/os2loop_cura_login/src/IdPHelper.php b/web/profiles/custom/os2loop/modules/os2loop_cura_login/src/IdPHelper.php index 2588b5c91..2c3a2e6a2 100644 --- a/web/profiles/custom/os2loop/modules/os2loop_cura_login/src/IdPHelper.php +++ b/web/profiles/custom/os2loop/modules/os2loop_cura_login/src/IdPHelper.php @@ -3,11 +3,14 @@ namespace Drupal\os2loop_cura_login; use Drupal\os2loop_cura_login\Settings\IdP; +use Drupal\os2loop_cura_login\Trait\ControllerAwareTrait; /** * IdP helper. */ final class IdPHelper { + use ControllerAwareTrait; + /** * The settings. */ @@ -23,31 +26,44 @@ public function __construct( * Get user info from userinfo endpoint. */ public function fetchUserinfo(string $username): array { - $query = [ - $this->settings->getUsernameClaim() => $username, - ]; + // @todo Call some API here … + // $query = [ + // $this->settings->getUsernameClaim() => $username, + // ]; // $result = fetch($query) - return [ + // Mock up some user data matching claims from OIDC login. + $result = [ // Drupal user fields. + 'upn' => $username, 'name' => $username, - 'mail' => $username . '@cura.example.com', - - // OS2Lloop fields - // 'os2loop_user_address' => '', - // 'os2loop_user_areas_of_expertise' => '', - // 'os2loop_user_biography' => '', - // 'os2loop_user_city' => '', - // 'os2loop_user_external_list' => '',. - 'os2loop_user_family_name' => 'Cura', - 'os2loop_user_given_name' => 'User', - // 'os2loop_user_image' => '', - // 'os2loop_user_internal_list' => '', - // 'os2loop_user_job_title' => '', - // 'os2loop_user_phone_number' => '', - // 'os2loop_user_place' => '', - // 'os2loop_user_postal_code' => '', - // 'os2loop_user_professions' => '', + 'email' => filter_var($username, FILTER_VALIDATE_EMAIL) ? $username : $username . '@cura.example.com', + 'samaccountname' => $username, + 'given_name' => 'Cura', + 'family_name' => 'User', + 'groups' => [ + 'GG-Rolle-B2C-Loop-AuthenticatedUser-Prod', + // 'GG-Rolle-B2C-Loop-Administrator-Prod', + // 'GG-Rolle-B2C-Loop-Administrator-Test', + // 'GG-Rolle-B2C-Loop-DocumentAuthor-Prod', + // 'GG-Rolle-B2C-Loop-DocumentAuthor-Test', + // 'GG-Rolle-B2C-Loop-DocumentCollectionEditor-Prod', + // 'GG-Rolle-B2C-Loop-DocumentCollectionEditor-Test', + // 'GG-Rolle-B2C-Loop-DocumentationCoordinator-Prod', + // 'GG-Rolle-B2C-Loop-DocumentationCoordinator-Test', + // 'GG-Rolle-B2C-Loop-ExternalSourcesEditor-Prod', + // 'GG-Rolle-B2C-Loop-ExternalSourcesEditor-Test', + // 'GG-Rolle-B2C-Loop-Manager-Prod', + // 'GG-Rolle-B2C-Loop-Manager-Test', + // 'GG-Rolle-B2C-Loop-PostAuthor-Prod', + // 'GG-Rolle-B2C-Loop-PostAuthor-Test', + // 'GG-Rolle-B2C-Loop-ReadOnly-Prod', + // 'GG-Rolle-B2C-Loop-ReadOnly-Test', + // 'GG-Rolle-B2C-Loop-UserAdministrator-Prod', + // 'GG-Rolle-B2C-Loop-UserAdministrator-Test', + ], ]; + + return $result; } } diff --git a/web/profiles/custom/os2loop/modules/os2loop_cura_login/src/Settings.php b/web/profiles/custom/os2loop/modules/os2loop_cura_login/src/Settings.php index f8f86f72f..0f732ede1 100644 --- a/web/profiles/custom/os2loop/modules/os2loop_cura_login/src/Settings.php +++ b/web/profiles/custom/os2loop/modules/os2loop_cura_login/src/Settings.php @@ -68,7 +68,7 @@ public function getIdpSettings() { * Get log level. */ public function getLogLevel() { - return (int) $this->config->get(self::SETTING_LOG_LEVEL) ?? RfcLogLevel::ERROR; + return (int) ($this->config->get(self::NAME)[self::SETTING_LOG_LEVEL] ?? RfcLogLevel::ERROR); } /** diff --git a/web/profiles/custom/os2loop/modules/os2loop_cura_login/src/Trait/ControllerAwareTrait.php b/web/profiles/custom/os2loop/modules/os2loop_cura_login/src/Trait/ControllerAwareTrait.php new file mode 100644 index 000000000..bfbf520ce --- /dev/null +++ b/web/profiles/custom/os2loop/modules/os2loop_cura_login/src/Trait/ControllerAwareTrait.php @@ -0,0 +1,35 @@ +controller = $controller; + } + + /** + * {@inheritdoc} + */ + public function log($level, \Stringable|string $message, array $context = []): void { + if ($this->controller) { + $this->controller->log($level, $message, $context); + } + } + +} diff --git a/web/profiles/custom/os2loop/modules/os2loop_cura_login/src/UserHelper.php b/web/profiles/custom/os2loop/modules/os2loop_cura_login/src/UserHelper.php index b4e4a6744..af98ec1a7 100644 --- a/web/profiles/custom/os2loop/modules/os2loop_cura_login/src/UserHelper.php +++ b/web/profiles/custom/os2loop/modules/os2loop_cura_login/src/UserHelper.php @@ -3,19 +3,26 @@ namespace Drupal\os2loop_cura_login; use Drupal\Core\Entity\EntityTypeManagerInterface; +use Drupal\openid_connect\OpenIDConnect; +use Drupal\os2loop_cura_login\Trait\ControllerAwareTrait; use Drupal\user\UserInterface; use Drupal\user\UserStorageInterface; +use Symfony\Component\DependencyInjection\Attribute\Autowire; /** * User helper. */ final class UserHelper { + use ControllerAwareTrait; + /** * The user storage. */ private readonly UserStorageInterface $userStorage; public function __construct( + #[Autowire(service: 'openid_connect.openid_connect')] + private readonly OpenIDConnect $openidConnect, EntityTypeManagerInterface $entityTypeManager, ) { $this->userStorage = $entityTypeManager->getStorage('user'); @@ -39,17 +46,21 @@ public function ensureUser(string $username, array $userinfo): UserInterface { $user = $this->userStorage->create(); } - foreach ($userinfo as $field => $value) { - $currentValue = $user->get($field); - if ($currentValue !== $value) { - $user->set($field, $value); - } + // Make sure that the user is active. + $user->activate(); + // saveUserinfo below needs a user id (uid). + if ($user->isNew()) { + $user->setUsername($username); + $user->save(); } - // Make sure that the user is active. - $user - ->activate() - ->save(); + // We piggyback on the OpenId Connect module to set user fields and roles. + if ($this->openidConnect->saveUserinfo($user, ['userinfo' => $userinfo])) { + $this->info('Userinfo saved on user @user (@username)', ['@user' => $user->label(), '@username' => $username]); + } + else { + $this->error('Error saving info on user @user (@username)', ['@user' => $user->label(), '@username' => $username]); + } return $user; } From 0fed3f491f646c9a81dadd48ae65e5f991bc59cb Mon Sep 17 00:00:00 2001 From: Mikkel Ricky Date: Mon, 8 Sep 2025 15:11:14 +0200 Subject: [PATCH 15/15] Update web/profiles/custom/os2loop/modules/os2loop_cura_login/src/Controller/Os2loopCuraLoginController.php Co-authored-by: Sine Jespersen --- .../src/Controller/Os2loopCuraLoginController.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/profiles/custom/os2loop/modules/os2loop_cura_login/src/Controller/Os2loopCuraLoginController.php b/web/profiles/custom/os2loop/modules/os2loop_cura_login/src/Controller/Os2loopCuraLoginController.php index 49eb9799a..92676e11f 100644 --- a/web/profiles/custom/os2loop/modules/os2loop_cura_login/src/Controller/Os2loopCuraLoginController.php +++ b/web/profiles/custom/os2loop/modules/os2loop_cura_login/src/Controller/Os2loopCuraLoginController.php @@ -166,7 +166,7 @@ private function createAuthenticateResponse(UserInterface $user, Request $reques $payload = [ // Issued at. 'iat' => $this->time->getRequestTime(), - // Expire af 60 seconds. + // Expire after 60 seconds. 'exp' => $this->time->getRequestTime() + 60, 'username' => $user->getAccountName(), ];