Skip to content

Commit ae86fd7

Browse files
authored
Merge branch 'main' into nmc/2372-central-customization-setup
2 parents 70fd265 + 60d84ee commit ae86fd7

25 files changed

Lines changed: 1103 additions & 100 deletions

.github/workflows/integration.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -95,11 +95,11 @@ jobs:
9595
working-directory: apps/${{ env.APP_NAME }}/tests/
9696

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

105105
- name: Set up PHPUnit

.github/workflows/reuse.yml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ jobs:
1717
steps:
1818
- name: Checkout
1919
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
20+
with:
21+
persist-credentials: false
2022

2123
- name: REUSE Compliance Check
22-
uses: fsfe/reuse-action@3ae3c6bdf1257ab19397fab11fd3312144692083 # v4.0.0
24+
uses: fsfe/reuse-action@bb774aa972c2a89ff34781233d275075cbddf542 # v5.0.0

README.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -287,6 +287,15 @@ You can disable these check with these config value (in config.php):
287287
],
288288
```
289289

290+
### Disable the user search by email
291+
292+
This app can stop matching users (when a user search is performed in Nextcloud) by setting this config.php value:
293+
``` php
294+
'user_oidc' => [
295+
'user_search_match_emails' => false,
296+
],
297+
```
298+
290299
## Building the app
291300

292301
Requirements for building:

docs/token_exchange.md

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
<!--
2+
- SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
3+
- SPDX-License-Identifier: AGPL-3.0-or-later
4+
-->
5+
### Token exchange
6+
7+
If your IdP supports token exchange, user_oidc can exchange the login token against another token.
8+
9+
Keycloak supports token exchange if its "Preview" mode is enabled. See https://www.keycloak.org/securing-apps/token-exchange .
10+
11+
:warning: Your IdP need to be configured accordingly. For example, Keycloak requires that token exchange is explicitely
12+
authorized for the target Oidc client.
13+
14+
The type of token exchange that user_oidc can perform is "Internal token to internal token"
15+
(https://www.keycloak.org/securing-apps/token-exchange#_internal-token-to-internal-token-exchange).
16+
This means you can exchange a token delivered for an audience "A" for a token delivered for an audience "B".
17+
In other words, you can get a token of a different Oidc client than the one you configured in user_oidc.
18+
19+
In short, you don't need the client ID and client secret of the target audience's client.
20+
Providing a token for the audience "A" (the login token) is enough to obtain a token for the audience "B".
21+
22+
user_oidc is storing the login token in the user's Nextcloud session and takes care of refreshing it when needed.
23+
When another app wants to exchange the current login token for another one,
24+
it can dispatch the `OCA\UserOIDC\Event\ExchangedTokenRequestedEvent` event.
25+
The exchanged token is immediately stored in the event object itself.
26+
27+
```php
28+
if (class_exists('OCA\UserOIDC\Event\ExchangedTokenRequestedEvent')) {
29+
$event = new OCA\UserOIDC\Event\ExchangedTokenRequestedEvent('my_target_audience');
30+
try {
31+
$this->eventDispatcher->dispatchTyped($event);
32+
} catch (OCA\UserOIDC\Exception\TokenExchangeFailedException $e) {
33+
$this->logger->debug('Failed to exchange token: ' . $e->getMessage());
34+
$error = $e->getError();
35+
$errorDescription = $e->getErrorDescription();
36+
if ($error && $errorDescription) {
37+
$this->logger->debug('Token exchange error response from the IdP: ' . $error . ' (' . $errorDescription . ')');
38+
}
39+
}
40+
$token = $event->getToken();
41+
if ($token === null) {
42+
$this->logger->debug('ExchangedTokenRequestedEvent event has not been caught by user_oidc');
43+
} else {
44+
$this->logger->debug('Obtained a token that expires in ' . $token->getExpiresInFromNow());
45+
// use the token
46+
$accessToken = $token->getAccessToken();
47+
}
48+
} else {
49+
$this->logger->debug('The user_oidc app is not installed/available');
50+
}
51+
```

lib/AppInfo/Application.php

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@
1313
use OC_User;
1414
use OCA\Files\Event\LoadAdditionalScriptsEvent;
1515
use OCA\UserOIDC\Db\ProviderMapper;
16+
use OCA\UserOIDC\Event\ExchangedTokenRequestedEvent;
17+
use OCA\UserOIDC\Listener\ExchangedTokenRequestedListener;
1618
use OCA\UserOIDC\Listener\TimezoneHandlingListener;
1719
use OCA\UserOIDC\MagentaBearer\MBackend;
1820
use OCA\UserOIDC\Service\ID4MeService;
@@ -68,10 +70,12 @@ public function register(IRegistrationContext $context): void {
6870
OC_User::useBackend($this->backend);
6971

7072
$context->registerEventListener(LoadAdditionalScriptsEvent::class, TimezoneHandlingListener::class);
73+
$context->registerEventListener(ExchangedTokenRequestedEvent::class, ExchangedTokenRequestedListener::class);
7174
}
7275

7376
public function boot(IBootContext $context): void {
7477
$context->injectFn(\Closure::fromCallable([$this->backend, 'injectSession']));
78+
$context->injectFn(\Closure::fromCallable([$this, 'checkLoginToken']));
7579
/** @var IUserSession $userSession */
7680
$userSession = $this->getContainer()->get(IUserSession::class);
7781
if ($userSession->isLoggedIn()) {
@@ -149,6 +153,7 @@ private function registerNmcClientFlow(IRequest $request,
149153
private function registerRedirect(IRequest $request, IURLGenerator $urlGenerator, SettingsService $settings, ProviderMapper $providerMapper): void {
150154
$providers = $this->getCachedProviders($providerMapper);
151155
$redirectUrl = $request->getParam('redirect_url');
156+
$absoluteRedirectUrl = !empty($redirectUrl) ? $urlGenerator->getAbsoluteURL($redirectUrl) : $redirectUrl;
152157

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

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

lib/Command/UpsertProvider.php

Lines changed: 34 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -24,107 +24,111 @@ class UpsertProvider extends Base {
2424

2525
private const EXTRA_OPTIONS = [
2626
'unique-uid' => [
27-
'shortcut' => null, 'mode' => InputOption::VALUE_OPTIONAL, 'default' => null, 'setting_key' => ProviderService::SETTING_UNIQUE_UID,
28-
'description' => 'Flag if unique user ids shall be used or not. 1 to enable (default), 0 to disable',
27+
'shortcut' => null, 'mode' => InputOption::VALUE_REQUIRED, 'setting_key' => ProviderService::SETTING_UNIQUE_UID,
28+
'description' => 'Determines if unique user ids shall be used or not. 1 to enable, 0 to disable',
2929
],
3030
'check-bearer' => [
31-
'shortcut' => null, 'mode' => InputOption::VALUE_OPTIONAL, 'default' => null, 'setting_key' => ProviderService::SETTING_CHECK_BEARER,
32-
'description' => 'Flag if Nextcloud API/WebDav calls should check the Bearer token against this provider or not. 1 to enable (default), 0 to disable',
31+
'shortcut' => null, 'mode' => InputOption::VALUE_REQUIRED, 'setting_key' => ProviderService::SETTING_CHECK_BEARER,
32+
'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)',
33+
],
34+
'bearer-provisioning' => [
35+
'shortcut' => null, 'mode' => InputOption::VALUE_REQUIRED, 'setting_key' => ProviderService::SETTING_BEARER_PROVISIONING,
36+
'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)',
3337
],
3438
'send-id-token-hint' => [
35-
'shortcut' => null, 'mode' => InputOption::VALUE_OPTIONAL, 'default' => null, 'setting_key' => ProviderService::SETTING_SEND_ID_TOKEN_HINT,
36-
'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',
39+
'shortcut' => null, 'mode' => InputOption::VALUE_REQUIRED, 'setting_key' => ProviderService::SETTING_SEND_ID_TOKEN_HINT,
40+
'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',
3741
],
3842
'mapping-display-name' => [
39-
'shortcut' => null, 'mode' => InputOption::VALUE_OPTIONAL, 'default' => null, 'setting_key' => ProviderService::SETTING_MAPPING_DISPLAYNAME,
43+
'shortcut' => null, 'mode' => InputOption::VALUE_REQUIRED, 'setting_key' => ProviderService::SETTING_MAPPING_DISPLAYNAME,
4044
'description' => 'Attribute mapping of the display name',
4145
],
4246
'mapping-email' => [
43-
'shortcut' => null, 'mode' => InputOption::VALUE_OPTIONAL, 'default' => null, 'setting_key' => ProviderService::SETTING_MAPPING_EMAIL,
47+
'shortcut' => null, 'mode' => InputOption::VALUE_REQUIRED, 'setting_key' => ProviderService::SETTING_MAPPING_EMAIL,
4448
'description' => 'Attribute mapping of the email address',
4549
],
4650
'mapping-quota' => [
47-
'shortcut' => null, 'mode' => InputOption::VALUE_OPTIONAL, 'default' => null, 'setting_key' => ProviderService::SETTING_MAPPING_QUOTA,
51+
'shortcut' => null, 'mode' => InputOption::VALUE_REQUIRED, 'setting_key' => ProviderService::SETTING_MAPPING_QUOTA,
4852
'description' => 'Attribute mapping of the quota',
4953
],
5054
'mapping-uid' => [
51-
'shortcut' => null, 'mode' => InputOption::VALUE_OPTIONAL, 'default' => null, 'setting_key' => ProviderService::SETTING_MAPPING_UID,
55+
'shortcut' => null, 'mode' => InputOption::VALUE_REQUIRED, 'setting_key' => ProviderService::SETTING_MAPPING_UID,
5256
'description' => 'Attribute mapping of the user id',
5357
],
5458
'extra-claims' => [
55-
'shortcut' => null, 'mode' => InputOption::VALUE_OPTIONAL, 'default' => null, 'setting_key' => ProviderService::SETTING_EXTRA_CLAIMS,
59+
'shortcut' => null, 'mode' => InputOption::VALUE_REQUIRED, 'setting_key' => ProviderService::SETTING_EXTRA_CLAIMS,
5660
'description' => 'Extra claims to request when getting tokens',
5761
],
5862
'mapping-website' => [
59-
'shortcut' => null, 'mode' => InputOption::VALUE_OPTIONAL, 'default' => null, 'setting_key' => ProviderService::SETTING_MAPPING_WEBSITE,
63+
'shortcut' => null, 'mode' => InputOption::VALUE_REQUIRED, 'setting_key' => ProviderService::SETTING_MAPPING_WEBSITE,
6064
'description' => 'Attribute mapping of the website',
6165
],
6266
'mapping-avatar' => [
63-
'shortcut' => null, 'mode' => InputOption::VALUE_OPTIONAL, 'default' => null, 'setting_key' => ProviderService::SETTING_MAPPING_AVATAR,
67+
'shortcut' => null, 'mode' => InputOption::VALUE_REQUIRED, 'setting_key' => ProviderService::SETTING_MAPPING_AVATAR,
6468
'description' => 'Attribute mapping of the avatar',
6569
],
6670
'mapping-twitter' => [
67-
'shortcut' => null, 'mode' => InputOption::VALUE_OPTIONAL, 'default' => null, 'setting_key' => ProviderService::SETTING_MAPPING_TWITTER,
71+
'shortcut' => null, 'mode' => InputOption::VALUE_REQUIRED, 'setting_key' => ProviderService::SETTING_MAPPING_TWITTER,
6872
'description' => 'Attribute mapping of twitter',
6973
],
7074
'mapping-fediverse' => [
71-
'shortcut' => null, 'mode' => InputOption::VALUE_OPTIONAL, 'default' => null, 'setting_key' => ProviderService::SETTING_MAPPING_FEDIVERSE,
75+
'shortcut' => null, 'mode' => InputOption::VALUE_REQUIRED, 'setting_key' => ProviderService::SETTING_MAPPING_FEDIVERSE,
7276
'description' => 'Attribute mapping of the fediverse',
7377
],
7478
'mapping-organisation' => [
75-
'shortcut' => null, 'mode' => InputOption::VALUE_OPTIONAL, 'default' => null, 'setting_key' => ProviderService::SETTING_MAPPING_ORGANISATION,
79+
'shortcut' => null, 'mode' => InputOption::VALUE_REQUIRED, 'setting_key' => ProviderService::SETTING_MAPPING_ORGANISATION,
7680
'description' => 'Attribute mapping of the organisation',
7781
],
7882
'mapping-role' => [
79-
'shortcut' => null, 'mode' => InputOption::VALUE_OPTIONAL, 'default' => null, 'setting_key' => ProviderService::SETTING_MAPPING_ROLE,
83+
'shortcut' => null, 'mode' => InputOption::VALUE_REQUIRED, 'setting_key' => ProviderService::SETTING_MAPPING_ROLE,
8084
'description' => 'Attribute mapping of the role',
8185
],
8286
'mapping-headline' => [
83-
'shortcut' => null, 'mode' => InputOption::VALUE_OPTIONAL, 'default' => null, 'setting_key' => ProviderService::SETTING_MAPPING_HEADLINE,
87+
'shortcut' => null, 'mode' => InputOption::VALUE_REQUIRED, 'setting_key' => ProviderService::SETTING_MAPPING_HEADLINE,
8488
'description' => 'Attribute mapping of the headline',
8589
],
8690
'mapping-biography' => [
87-
'shortcut' => null, 'mode' => InputOption::VALUE_OPTIONAL, 'default' => null, 'setting_key' => ProviderService::SETTING_MAPPING_BIOGRAPHY,
91+
'shortcut' => null, 'mode' => InputOption::VALUE_REQUIRED, 'setting_key' => ProviderService::SETTING_MAPPING_BIOGRAPHY,
8892
'description' => 'Attribute mapping of the biography',
8993
],
9094
'mapping-phone' => [
91-
'shortcut' => null, 'mode' => InputOption::VALUE_OPTIONAL, 'default' => null, 'setting_key' => ProviderService::SETTING_MAPPING_PHONE,
95+
'shortcut' => null, 'mode' => InputOption::VALUE_REQUIRED, 'setting_key' => ProviderService::SETTING_MAPPING_PHONE,
9296
'description' => 'Attribute mapping of the phone',
9397
],
9498
'mapping-gender' => [
95-
'shortcut' => null, 'mode' => InputOption::VALUE_OPTIONAL, 'default' => null, 'setting_key' => ProviderService::SETTING_MAPPING_GENDER,
99+
'shortcut' => null, 'mode' => InputOption::VALUE_REQUIRED, 'setting_key' => ProviderService::SETTING_MAPPING_GENDER,
96100
'description' => 'Attribute mapping of the gender',
97101
],
98102
'mapping-address' => [
99-
'shortcut' => null, 'mode' => InputOption::VALUE_OPTIONAL, 'default' => null, 'setting_key' => ProviderService::SETTING_MAPPING_ADDRESS,
103+
'shortcut' => null, 'mode' => InputOption::VALUE_REQUIRED, 'setting_key' => ProviderService::SETTING_MAPPING_ADDRESS,
100104
'description' => 'Attribute mapping of the address',
101105
],
102106
'mapping-street_address' => [
103-
'shortcut' => null, 'mode' => InputOption::VALUE_OPTIONAL, 'default' => null, 'setting_key' => ProviderService::SETTING_MAPPING_STREETADDRESS,
107+
'shortcut' => null, 'mode' => InputOption::VALUE_REQUIRED, 'setting_key' => ProviderService::SETTING_MAPPING_STREETADDRESS,
104108
'description' => 'Attribute mapping of the street address',
105109
],
106110
'mapping-postal_code' => [
107-
'shortcut' => null, 'mode' => InputOption::VALUE_OPTIONAL, 'default' => null, 'setting_key' => ProviderService::SETTING_MAPPING_POSTALCODE,
111+
'shortcut' => null, 'mode' => InputOption::VALUE_REQUIRED, 'setting_key' => ProviderService::SETTING_MAPPING_POSTALCODE,
108112
'description' => 'Attribute mapping of the postal code',
109113
],
110114
'mapping-locality' => [
111-
'shortcut' => null, 'mode' => InputOption::VALUE_OPTIONAL, 'default' => null, 'setting_key' => ProviderService::SETTING_MAPPING_LOCALITY,
115+
'shortcut' => null, 'mode' => InputOption::VALUE_REQUIRED, 'setting_key' => ProviderService::SETTING_MAPPING_LOCALITY,
112116
'description' => 'Attribute mapping of the locality',
113117
],
114118
'mapping-region' => [
115-
'shortcut' => null, 'mode' => InputOption::VALUE_OPTIONAL, 'default' => null, 'setting_key' => ProviderService::SETTING_MAPPING_REGION,
119+
'shortcut' => null, 'mode' => InputOption::VALUE_REQUIRED, 'setting_key' => ProviderService::SETTING_MAPPING_REGION,
116120
'description' => 'Attribute mapping of the region',
117121
],
118122
'mapping-country' => [
119-
'shortcut' => null, 'mode' => InputOption::VALUE_OPTIONAL, 'default' => null, 'setting_key' => ProviderService::SETTING_MAPPING_COUNTRY,
123+
'shortcut' => null, 'mode' => InputOption::VALUE_REQUIRED, 'setting_key' => ProviderService::SETTING_MAPPING_COUNTRY,
120124
'description' => 'Attribute mapping of the country',
121125
],
122126
'group-provisioning' => [
123-
'shortcut' => null, 'mode' => InputOption::VALUE_OPTIONAL, 'default' => null, 'setting_key' => ProviderService::SETTING_GROUP_PROVISIONING,
127+
'shortcut' => null, 'mode' => InputOption::VALUE_REQUIRED, 'setting_key' => ProviderService::SETTING_GROUP_PROVISIONING,
124128
'description' => 'Flag to toggle group provisioning. 1 to enable, 0 to disable (default)',
125129
],
126130
'mapping-groups' => [
127-
'shortcut' => null, 'mode' => InputOption::VALUE_OPTIONAL, 'default' => null, 'setting_key' => ProviderService::SETTING_MAPPING_GROUPS,
131+
'shortcut' => null, 'mode' => InputOption::VALUE_REQUIRED, 'setting_key' => ProviderService::SETTING_MAPPING_GROUPS,
128132
'description' => 'Attribute mapping of the groups',
129133
],
130134
];
@@ -148,7 +152,7 @@ protected function configure() {
148152
->addOption('endsessionendpointuri', 'e', InputOption::VALUE_OPTIONAL, 'OpenID end session endpoint uri')
149153
->addOption('scope', 'o', InputOption::VALUE_OPTIONAL, 'OpenID requested value scopes, if not set defaults to "openid email profile"');
150154
foreach (self::EXTRA_OPTIONS as $name => $option) {
151-
$this->addOption($name, $option['shortcut'], $option['mode'], $option['description'], $option['default']);
155+
$this->addOption($name, $option['shortcut'], $option['mode'], $option['description']);
152156
}
153157
parent::configure();
154158
}

0 commit comments

Comments
 (0)