Skip to content

Commit

Permalink
Merge branch 'main' into nmc/2372-central-customization-setup
Browse files Browse the repository at this point in the history
  • Loading branch information
memurats authored Jan 14, 2025
2 parents 70fd265 + 60d84ee commit ae86fd7
Show file tree
Hide file tree
Showing 25 changed files with 1,103 additions and 100 deletions.
4 changes: 2 additions & 2 deletions .github/workflows/integration.yml
Original file line number Diff line number Diff line change
Expand Up @@ -95,11 +95,11 @@ jobs:
working-directory: apps/${{ env.APP_NAME }}/tests/

- name: Set up php ${{ matrix.php-versions }}
uses: shivammathur/setup-php@2.12.0
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php-versions }}
tools: phpunit
extensions: mbstring, iconv, fileinfo, intl, sqlite, pdo_sqlite, mysql, pdo_mysql, pgsql, pdo_pgsql,
extensions: zip, gd, mbstring, iconv, fileinfo, intl, sqlite, pdo_sqlite, mysql, pdo_mysql, pgsql, pdo_pgsql
coverage: none

- name: Set up PHPUnit
Expand Down
4 changes: 3 additions & 1 deletion .github/workflows/reuse.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
persist-credentials: false

- name: REUSE Compliance Check
uses: fsfe/reuse-action@3ae3c6bdf1257ab19397fab11fd3312144692083 # v4.0.0
uses: fsfe/reuse-action@bb774aa972c2a89ff34781233d275075cbddf542 # v5.0.0
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -287,6 +287,15 @@ You can disable these check with these config value (in config.php):
],
```

### Disable the user search by email

This app can stop matching users (when a user search is performed in Nextcloud) by setting this config.php value:
``` php
'user_oidc' => [
'user_search_match_emails' => false,
],
```

## Building the app

Requirements for building:
Expand Down
51 changes: 51 additions & 0 deletions docs/token_exchange.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
<!--
- SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
- SPDX-License-Identifier: AGPL-3.0-or-later
-->
### Token exchange

If your IdP supports token exchange, user_oidc can exchange the login token against another token.

Keycloak supports token exchange if its "Preview" mode is enabled. See https://www.keycloak.org/securing-apps/token-exchange .

:warning: Your IdP need to be configured accordingly. For example, Keycloak requires that token exchange is explicitely
authorized for the target Oidc client.

The type of token exchange that user_oidc can perform is "Internal token to internal token"
(https://www.keycloak.org/securing-apps/token-exchange#_internal-token-to-internal-token-exchange).
This means you can exchange a token delivered for an audience "A" for a token delivered for an audience "B".
In other words, you can get a token of a different Oidc client than the one you configured in user_oidc.

In short, you don't need the client ID and client secret of the target audience's client.
Providing a token for the audience "A" (the login token) is enough to obtain a token for the audience "B".

user_oidc is storing the login token in the user's Nextcloud session and takes care of refreshing it when needed.
When another app wants to exchange the current login token for another one,
it can dispatch the `OCA\UserOIDC\Event\ExchangedTokenRequestedEvent` event.
The exchanged token is immediately stored in the event object itself.

```php
if (class_exists('OCA\UserOIDC\Event\ExchangedTokenRequestedEvent')) {
$event = new OCA\UserOIDC\Event\ExchangedTokenRequestedEvent('my_target_audience');
try {
$this->eventDispatcher->dispatchTyped($event);
} catch (OCA\UserOIDC\Exception\TokenExchangeFailedException $e) {
$this->logger->debug('Failed to exchange token: ' . $e->getMessage());
$error = $e->getError();
$errorDescription = $e->getErrorDescription();
if ($error && $errorDescription) {
$this->logger->debug('Token exchange error response from the IdP: ' . $error . ' (' . $errorDescription . ')');
}
}
$token = $event->getToken();
if ($token === null) {
$this->logger->debug('ExchangedTokenRequestedEvent event has not been caught by user_oidc');
} else {
$this->logger->debug('Obtained a token that expires in ' . $token->getExpiresInFromNow());
// use the token
$accessToken = $token->getAccessToken();
}
} else {
$this->logger->debug('The user_oidc app is not installed/available');
}
```
10 changes: 8 additions & 2 deletions lib/AppInfo/Application.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@
use OC_User;
use OCA\Files\Event\LoadAdditionalScriptsEvent;
use OCA\UserOIDC\Db\ProviderMapper;
use OCA\UserOIDC\Event\ExchangedTokenRequestedEvent;
use OCA\UserOIDC\Listener\ExchangedTokenRequestedListener;
use OCA\UserOIDC\Listener\TimezoneHandlingListener;
use OCA\UserOIDC\MagentaBearer\MBackend;
use OCA\UserOIDC\Service\ID4MeService;
Expand Down Expand Up @@ -68,10 +70,12 @@ public function register(IRegistrationContext $context): void {
OC_User::useBackend($this->backend);

$context->registerEventListener(LoadAdditionalScriptsEvent::class, TimezoneHandlingListener::class);
$context->registerEventListener(ExchangedTokenRequestedEvent::class, ExchangedTokenRequestedListener::class);
}

public function boot(IBootContext $context): void {
$context->injectFn(\Closure::fromCallable([$this->backend, 'injectSession']));
$context->injectFn(\Closure::fromCallable([$this, 'checkLoginToken']));

Check failure on line 78 in lib/AppInfo/Application.php

View workflow job for this annotation

GitHub Actions / static-psalm-analysis

InvalidArgument

lib/AppInfo/Application.php:78:45: InvalidArgument: Argument 1 of Closure::fromCallable expects callable, but list{OCA\UserOIDC\AppInfo\Application&static, 'checkLoginToken'} provided (see https://psalm.dev/004)
/** @var IUserSession $userSession */
$userSession = $this->getContainer()->get(IUserSession::class);
if ($userSession->isLoggedIn()) {
Expand Down Expand Up @@ -149,6 +153,7 @@ private function registerNmcClientFlow(IRequest $request,
private function registerRedirect(IRequest $request, IURLGenerator $urlGenerator, SettingsService $settings, ProviderMapper $providerMapper): void {
$providers = $this->getCachedProviders($providerMapper);
$redirectUrl = $request->getParam('redirect_url');
$absoluteRedirectUrl = !empty($redirectUrl) ? $urlGenerator->getAbsoluteURL($redirectUrl) : $redirectUrl;

// Handle immediate redirect to the oidc provider if just one is configured and no other backends are allowed
$isDefaultLogin = false;
Expand All @@ -160,7 +165,7 @@ private function registerRedirect(IRequest $request, IURLGenerator $urlGenerator
if ($isDefaultLogin && !$settings->getAllowMultipleUserBackEnds() && count($providers) === 1) {
$targetUrl = $urlGenerator->linkToRoute(self::APP_ID . '.login.login', [
'providerId' => $providers[0]->getId(),
'redirectUrl' => $redirectUrl
'redirectUrl' => $absoluteRedirectUrl
]);
header('Location: ' . $targetUrl);
exit();
Expand All @@ -169,12 +174,13 @@ private function registerRedirect(IRequest $request, IURLGenerator $urlGenerator

private function registerLogin(IRequest $request, IL10N $l10n, IURLGenerator $urlGenerator, ProviderMapper $providerMapper): void {
$redirectUrl = $request->getParam('redirect_url');
$absoluteRedirectUrl = !empty($redirectUrl) ? $urlGenerator->getAbsoluteURL($redirectUrl) : $redirectUrl;
$providers = $this->getCachedProviders($providerMapper);
foreach ($providers as $provider) {
// FIXME: Move to IAlternativeLogin but requires boot due to db connection
OC_App::registerLogIn([
'name' => $l10n->t('Login with %1s', [$provider->getIdentifier()]),
'href' => $urlGenerator->linkToRoute(self::APP_ID . '.login.login', ['providerId' => $provider->getId(), 'redirectUrl' => $redirectUrl]),
'href' => $urlGenerator->linkToRoute(self::APP_ID . '.login.login', ['providerId' => $provider->getId(), 'redirectUrl' => $absoluteRedirectUrl]),
]);
}

Expand Down
64 changes: 34 additions & 30 deletions lib/Command/UpsertProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,107 +24,111 @@ class UpsertProvider extends Base {

private const EXTRA_OPTIONS = [
'unique-uid' => [
'shortcut' => null, 'mode' => InputOption::VALUE_OPTIONAL, 'default' => null, 'setting_key' => ProviderService::SETTING_UNIQUE_UID,
'description' => 'Flag if unique user ids shall be used or not. 1 to enable (default), 0 to disable',
'shortcut' => null, 'mode' => InputOption::VALUE_REQUIRED, 'setting_key' => ProviderService::SETTING_UNIQUE_UID,
'description' => 'Determines if unique user ids shall be used or not. 1 to enable, 0 to disable',
],
'check-bearer' => [
'shortcut' => null, 'mode' => InputOption::VALUE_OPTIONAL, 'default' => null, 'setting_key' => ProviderService::SETTING_CHECK_BEARER,
'description' => 'Flag if Nextcloud API/WebDav calls should check the Bearer token against this provider or not. 1 to enable (default), 0 to disable',
'shortcut' => null, 'mode' => InputOption::VALUE_REQUIRED, 'setting_key' => ProviderService::SETTING_CHECK_BEARER,
'description' => 'Determines if Nextcloud API/WebDav calls should check the Bearer token against this provider or not. 1 to enable, 0 to disable (default when creating a new provider)',
],
'bearer-provisioning' => [
'shortcut' => null, 'mode' => InputOption::VALUE_REQUIRED, 'setting_key' => ProviderService::SETTING_BEARER_PROVISIONING,
'description' => 'Determines if Nextcloud API/WebDav calls should automatically provision the user, when sending API and WebDav Requests with a Bearer token. 1 to enable, 0 to disable (default when creating a new provider)',
],
'send-id-token-hint' => [
'shortcut' => null, 'mode' => InputOption::VALUE_OPTIONAL, 'default' => null, 'setting_key' => ProviderService::SETTING_SEND_ID_TOKEN_HINT,
'description' => 'Flag if ID token should be included as a parameter to the end_session_endpoint URL when using unified logout. 1 to enable (default), 0 to disable',
'shortcut' => null, 'mode' => InputOption::VALUE_REQUIRED, 'setting_key' => ProviderService::SETTING_SEND_ID_TOKEN_HINT,
'description' => 'Determines if ID token should be included as a parameter to the end_session_endpoint URL when using unified logout. 1 to enable, 0 to disable',
],
'mapping-display-name' => [
'shortcut' => null, 'mode' => InputOption::VALUE_OPTIONAL, 'default' => null, 'setting_key' => ProviderService::SETTING_MAPPING_DISPLAYNAME,
'shortcut' => null, 'mode' => InputOption::VALUE_REQUIRED, 'setting_key' => ProviderService::SETTING_MAPPING_DISPLAYNAME,
'description' => 'Attribute mapping of the display name',
],
'mapping-email' => [
'shortcut' => null, 'mode' => InputOption::VALUE_OPTIONAL, 'default' => null, 'setting_key' => ProviderService::SETTING_MAPPING_EMAIL,
'shortcut' => null, 'mode' => InputOption::VALUE_REQUIRED, 'setting_key' => ProviderService::SETTING_MAPPING_EMAIL,
'description' => 'Attribute mapping of the email address',
],
'mapping-quota' => [
'shortcut' => null, 'mode' => InputOption::VALUE_OPTIONAL, 'default' => null, 'setting_key' => ProviderService::SETTING_MAPPING_QUOTA,
'shortcut' => null, 'mode' => InputOption::VALUE_REQUIRED, 'setting_key' => ProviderService::SETTING_MAPPING_QUOTA,
'description' => 'Attribute mapping of the quota',
],
'mapping-uid' => [
'shortcut' => null, 'mode' => InputOption::VALUE_OPTIONAL, 'default' => null, 'setting_key' => ProviderService::SETTING_MAPPING_UID,
'shortcut' => null, 'mode' => InputOption::VALUE_REQUIRED, 'setting_key' => ProviderService::SETTING_MAPPING_UID,
'description' => 'Attribute mapping of the user id',
],
'extra-claims' => [
'shortcut' => null, 'mode' => InputOption::VALUE_OPTIONAL, 'default' => null, 'setting_key' => ProviderService::SETTING_EXTRA_CLAIMS,
'shortcut' => null, 'mode' => InputOption::VALUE_REQUIRED, 'setting_key' => ProviderService::SETTING_EXTRA_CLAIMS,
'description' => 'Extra claims to request when getting tokens',
],
'mapping-website' => [
'shortcut' => null, 'mode' => InputOption::VALUE_OPTIONAL, 'default' => null, 'setting_key' => ProviderService::SETTING_MAPPING_WEBSITE,
'shortcut' => null, 'mode' => InputOption::VALUE_REQUIRED, 'setting_key' => ProviderService::SETTING_MAPPING_WEBSITE,
'description' => 'Attribute mapping of the website',
],
'mapping-avatar' => [
'shortcut' => null, 'mode' => InputOption::VALUE_OPTIONAL, 'default' => null, 'setting_key' => ProviderService::SETTING_MAPPING_AVATAR,
'shortcut' => null, 'mode' => InputOption::VALUE_REQUIRED, 'setting_key' => ProviderService::SETTING_MAPPING_AVATAR,
'description' => 'Attribute mapping of the avatar',
],
'mapping-twitter' => [
'shortcut' => null, 'mode' => InputOption::VALUE_OPTIONAL, 'default' => null, 'setting_key' => ProviderService::SETTING_MAPPING_TWITTER,
'shortcut' => null, 'mode' => InputOption::VALUE_REQUIRED, 'setting_key' => ProviderService::SETTING_MAPPING_TWITTER,
'description' => 'Attribute mapping of twitter',
],
'mapping-fediverse' => [
'shortcut' => null, 'mode' => InputOption::VALUE_OPTIONAL, 'default' => null, 'setting_key' => ProviderService::SETTING_MAPPING_FEDIVERSE,
'shortcut' => null, 'mode' => InputOption::VALUE_REQUIRED, 'setting_key' => ProviderService::SETTING_MAPPING_FEDIVERSE,
'description' => 'Attribute mapping of the fediverse',
],
'mapping-organisation' => [
'shortcut' => null, 'mode' => InputOption::VALUE_OPTIONAL, 'default' => null, 'setting_key' => ProviderService::SETTING_MAPPING_ORGANISATION,
'shortcut' => null, 'mode' => InputOption::VALUE_REQUIRED, 'setting_key' => ProviderService::SETTING_MAPPING_ORGANISATION,
'description' => 'Attribute mapping of the organisation',
],
'mapping-role' => [
'shortcut' => null, 'mode' => InputOption::VALUE_OPTIONAL, 'default' => null, 'setting_key' => ProviderService::SETTING_MAPPING_ROLE,
'shortcut' => null, 'mode' => InputOption::VALUE_REQUIRED, 'setting_key' => ProviderService::SETTING_MAPPING_ROLE,
'description' => 'Attribute mapping of the role',
],
'mapping-headline' => [
'shortcut' => null, 'mode' => InputOption::VALUE_OPTIONAL, 'default' => null, 'setting_key' => ProviderService::SETTING_MAPPING_HEADLINE,
'shortcut' => null, 'mode' => InputOption::VALUE_REQUIRED, 'setting_key' => ProviderService::SETTING_MAPPING_HEADLINE,
'description' => 'Attribute mapping of the headline',
],
'mapping-biography' => [
'shortcut' => null, 'mode' => InputOption::VALUE_OPTIONAL, 'default' => null, 'setting_key' => ProviderService::SETTING_MAPPING_BIOGRAPHY,
'shortcut' => null, 'mode' => InputOption::VALUE_REQUIRED, 'setting_key' => ProviderService::SETTING_MAPPING_BIOGRAPHY,
'description' => 'Attribute mapping of the biography',
],
'mapping-phone' => [
'shortcut' => null, 'mode' => InputOption::VALUE_OPTIONAL, 'default' => null, 'setting_key' => ProviderService::SETTING_MAPPING_PHONE,
'shortcut' => null, 'mode' => InputOption::VALUE_REQUIRED, 'setting_key' => ProviderService::SETTING_MAPPING_PHONE,
'description' => 'Attribute mapping of the phone',
],
'mapping-gender' => [
'shortcut' => null, 'mode' => InputOption::VALUE_OPTIONAL, 'default' => null, 'setting_key' => ProviderService::SETTING_MAPPING_GENDER,
'shortcut' => null, 'mode' => InputOption::VALUE_REQUIRED, 'setting_key' => ProviderService::SETTING_MAPPING_GENDER,
'description' => 'Attribute mapping of the gender',
],
'mapping-address' => [
'shortcut' => null, 'mode' => InputOption::VALUE_OPTIONAL, 'default' => null, 'setting_key' => ProviderService::SETTING_MAPPING_ADDRESS,
'shortcut' => null, 'mode' => InputOption::VALUE_REQUIRED, 'setting_key' => ProviderService::SETTING_MAPPING_ADDRESS,
'description' => 'Attribute mapping of the address',
],
'mapping-street_address' => [
'shortcut' => null, 'mode' => InputOption::VALUE_OPTIONAL, 'default' => null, 'setting_key' => ProviderService::SETTING_MAPPING_STREETADDRESS,
'shortcut' => null, 'mode' => InputOption::VALUE_REQUIRED, 'setting_key' => ProviderService::SETTING_MAPPING_STREETADDRESS,
'description' => 'Attribute mapping of the street address',
],
'mapping-postal_code' => [
'shortcut' => null, 'mode' => InputOption::VALUE_OPTIONAL, 'default' => null, 'setting_key' => ProviderService::SETTING_MAPPING_POSTALCODE,
'shortcut' => null, 'mode' => InputOption::VALUE_REQUIRED, 'setting_key' => ProviderService::SETTING_MAPPING_POSTALCODE,
'description' => 'Attribute mapping of the postal code',
],
'mapping-locality' => [
'shortcut' => null, 'mode' => InputOption::VALUE_OPTIONAL, 'default' => null, 'setting_key' => ProviderService::SETTING_MAPPING_LOCALITY,
'shortcut' => null, 'mode' => InputOption::VALUE_REQUIRED, 'setting_key' => ProviderService::SETTING_MAPPING_LOCALITY,
'description' => 'Attribute mapping of the locality',
],
'mapping-region' => [
'shortcut' => null, 'mode' => InputOption::VALUE_OPTIONAL, 'default' => null, 'setting_key' => ProviderService::SETTING_MAPPING_REGION,
'shortcut' => null, 'mode' => InputOption::VALUE_REQUIRED, 'setting_key' => ProviderService::SETTING_MAPPING_REGION,
'description' => 'Attribute mapping of the region',
],
'mapping-country' => [
'shortcut' => null, 'mode' => InputOption::VALUE_OPTIONAL, 'default' => null, 'setting_key' => ProviderService::SETTING_MAPPING_COUNTRY,
'shortcut' => null, 'mode' => InputOption::VALUE_REQUIRED, 'setting_key' => ProviderService::SETTING_MAPPING_COUNTRY,
'description' => 'Attribute mapping of the country',
],
'group-provisioning' => [
'shortcut' => null, 'mode' => InputOption::VALUE_OPTIONAL, 'default' => null, 'setting_key' => ProviderService::SETTING_GROUP_PROVISIONING,
'shortcut' => null, 'mode' => InputOption::VALUE_REQUIRED, 'setting_key' => ProviderService::SETTING_GROUP_PROVISIONING,
'description' => 'Flag to toggle group provisioning. 1 to enable, 0 to disable (default)',
],
'mapping-groups' => [
'shortcut' => null, 'mode' => InputOption::VALUE_OPTIONAL, 'default' => null, 'setting_key' => ProviderService::SETTING_MAPPING_GROUPS,
'shortcut' => null, 'mode' => InputOption::VALUE_REQUIRED, 'setting_key' => ProviderService::SETTING_MAPPING_GROUPS,
'description' => 'Attribute mapping of the groups',
],
];
Expand All @@ -148,7 +152,7 @@ protected function configure() {
->addOption('endsessionendpointuri', 'e', InputOption::VALUE_OPTIONAL, 'OpenID end session endpoint uri')
->addOption('scope', 'o', InputOption::VALUE_OPTIONAL, 'OpenID requested value scopes, if not set defaults to "openid email profile"');
foreach (self::EXTRA_OPTIONS as $name => $option) {
$this->addOption($name, $option['shortcut'], $option['mode'], $option['description'], $option['default']);
$this->addOption($name, $option['shortcut'], $option['mode'], $option['description']);
}
parent::configure();
}
Expand Down
Loading

0 comments on commit ae86fd7

Please sign in to comment.