Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions AUTHORS.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,3 +40,4 @@
- Thibault Coupin <[email protected]>
- Tobias Wolter <[email protected]>
- Vincent Petry <[email protected]>
- Elvis Yerel Roman Concha <[email protected]>
45 changes: 45 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,51 @@ OpenID Connect user backend for Nextcloud
See [Nextcloud and OpenID-Connect](https://web.archive.org/web/20240412121655/https://www.schiessle.org/articles/2023/07/04/nextcloud-and-openid-connect/)
for a proper jumpstart.

---

## `user_oidc.httpclient.allowselfsigned`

```php
'user_oidc' => [
'httpclient.allowselfsigned' => true,
]
```

This configuration allows Nextcloud to **trust self-signed SSL certificates** when making HTTP requests through the internal HTTP client.
It is especially useful when your OAuth2 or OIDC provider is hosted locally or uses a self-signed certificate not recognized by public CAs.

* **true**: Disables SSL certificate verification (adds the `verify => false` option to the actual HTTP client)
* **false** (default): SSL verification remains enabled and strict

> ⚠️ Use with caution in production environments, as disabling certificate verification can introduce security risks.

---

## `user_oidc.prompt`

```php
'user_oidc' => [
'prompt' => 'internal'
]
```

This option allows customizing the `prompt` parameter sent in the OAuth2/OIDC authorization request.

Supported values include:

* `none`
* `login`
* `consent`
* `internal` (custom)

The `internal` prompt is specific to **[OAuth2 Passport Server](https://github.com/elyerr/oauth2-passport-server)** and is designed to enable seamless login
for private or internal applications without requiring user consent or interaction.

Documentation for all supported prompt values is available here:
[Oauth2 passport server prompts-supported](https://gitlab.com/elyerr/oauth2-passport-server/-/wikis/home/prompts-supported)

---

### User IDs

The OpenID Connect backend will ensure that user ids are unique even when multiple providers would report the same user
Expand Down
22 changes: 11 additions & 11 deletions composer.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

39 changes: 24 additions & 15 deletions lib/Controller/LoginController.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
use OCA\UserOIDC\Db\ProviderMapper;
use OCA\UserOIDC\Db\SessionMapper;
use OCA\UserOIDC\Event\TokenObtainedEvent;
use OCA\UserOIDC\Helper\HttpClientHelper;
use OCA\UserOIDC\Service\DiscoveryService;
use OCA\UserOIDC\Service\LdapService;
use OCA\UserOIDC\Service\OIDCService;
Expand All @@ -40,7 +41,6 @@
use OCP\Authentication\Token\IToken;
use OCP\DB\Exception;
use OCP\EventDispatcher\IEventDispatcher;
use OCP\Http\Client\IClientService;
use OCP\IConfig;
use OCP\IL10N;
use OCP\IRequest;
Expand Down Expand Up @@ -72,7 +72,7 @@ public function __construct(
private LdapService $ldapService,
private ISecureRandom $random,
private ISession $session,
private IClientService $clientService,
private HttpClientHelper $clientService,
private IURLGenerator $urlGenerator,
private IUserSession $userSession,
private IUserManager $userManager,
Expand Down Expand Up @@ -260,6 +260,8 @@ public function login(int $providerId, ?string $redirectUrl = null) {
}
}

$oidcConfig = $this->config->getSystemValue('user_oidc', []);

$data += [
'client_id' => $provider->getClientId(),
'response_type' => 'code',
Expand All @@ -268,11 +270,16 @@ public function login(int $providerId, ?string $redirectUrl = null) {
'claims' => json_encode($claims),
'state' => $state,
'nonce' => $nonce,
'prompt' => $oidcConfig['prompt'] ?? 'consent'
];


if ($isPkceEnabled) {
$data['code_challenge'] = $this->toCodeChallenge($code_verifier);
$data['code_challenge_method'] = 'S256';
}


$authorizationUrl = $this->discoveryService->buildAuthorizationUrl($discovery['authorization_endpoint'], $data);

$this->logger->debug('Redirecting user to: ' . $authorizationUrl);
Expand Down Expand Up @@ -356,7 +363,6 @@ public function code(string $state = '', string $code = '', string $scope = '',
$isPkceSupported = in_array('S256', $discovery['code_challenge_methods_supported'] ?? [], true);
$isPkceEnabled = $isPkceSupported && ($oidcSystemConfig['use_pkce'] ?? true);

$client = $this->clientService->newClient();
try {
$requestBody = [
'code' => $code,
Expand All @@ -370,10 +376,12 @@ public function code(string $state = '', string $code = '', string $scope = '',
$headers = [];
$tokenEndpointAuthMethod = 'client_secret_post';
// Use Basic only if client_secret_post is not available as supported by the endpoint
if (array_key_exists('token_endpoint_auth_methods_supported', $discovery) &&
is_array($discovery['token_endpoint_auth_methods_supported']) &&
in_array('client_secret_basic', $discovery['token_endpoint_auth_methods_supported']) &&
!in_array('client_secret_post', $discovery['token_endpoint_auth_methods_supported'])) {
if (
array_key_exists('token_endpoint_auth_methods_supported', $discovery)
&& is_array($discovery['token_endpoint_auth_methods_supported'])
&& in_array('client_secret_basic', $discovery['token_endpoint_auth_methods_supported'])
&& !in_array('client_secret_post', $discovery['token_endpoint_auth_methods_supported'])
) {
$tokenEndpointAuthMethod = 'client_secret_basic';
}

Expand All @@ -388,12 +396,10 @@ public function code(string $state = '', string $code = '', string $scope = '',
$requestBody['client_secret'] = $providerClientSecret;
}

$result = $client->post(
$body = $this->clientService->post(
$discovery['token_endpoint'],
[
'body' => $requestBody,
'headers' => $headers,
]
$requestBody,
$headers
);
} catch (ClientException|ServerException $e) {
$response = $e->getResponse();
Expand All @@ -417,7 +423,7 @@ public function code(string $state = '', string $code = '', string $scope = '',
return $this->build403TemplateResponse($message, Http::STATUS_FORBIDDEN, [], false);
}

$data = json_decode($result->getBody(), true);
$data = json_decode($body, true);
$this->logger->debug('Received code response: ' . json_encode($data, JSON_THROW_ON_ERROR));
$this->eventDispatcher->dispatchTyped(new TokenObtainedEvent($data, $provider, $discovery));

Expand Down Expand Up @@ -676,7 +682,8 @@ public function singleLogoutService() {
$endSessionEndpoint .= '&client_id=' . $provider->getClientId();
$shouldSendIdToken = $this->providerService->getSetting(
$provider->getId(),
ProviderService::SETTING_SEND_ID_TOKEN_HINT, '0'
ProviderService::SETTING_SEND_ID_TOKEN_HINT,
'0'
) === '1';
$idToken = $this->session->get(self::ID_TOKEN);
if ($shouldSendIdToken && $idToken) {
Expand Down Expand Up @@ -826,7 +833,9 @@ public function backChannelLogout(string $providerIdentifier, string $logout_tok
* @return JSONResponse
*/
private function getBackchannelLogoutErrorResponse(
string $error, string $description, array $throttleMetadata = [],
string $error,
string $description,
array $throttleMetadata = [],
): JSONResponse {
$this->logger->debug('Backchannel logout error. ' . $error . ' ; ' . $description);
return new JSONResponse(
Expand Down
29 changes: 22 additions & 7 deletions lib/Helper/HttpClientHelper.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,32 +8,47 @@

namespace OCA\UserOIDC\Helper;

use OCP\Http\Client\IClientService;
use OCP\IConfig;

require_once __DIR__ . '/../../vendor/autoload.php';
use Id4me\RP\HttpClient;
use OCP\Http\Client\IClientService;

class HttpClientHelper implements HttpClient {

public function __construct(
private IClientService $clientService,
private IConfig $config,
) {
}

public function get($url, array $headers = []) {
public function get($url, array $headers = [], array $options = []) {
$oidcConfig = $this->config->getSystemValue('user_oidc', []);

$client = $this->clientService->newClient();

return $client->get($url, [
'headers' => $headers,
])->getBody();
if (isset($oidcConfig['httpclient.allowselfsigned'])
&& !in_array($oidcConfig['httpclient.allowselfsigned'], [false, 'false', 0, '0'], true)) {
$options['verify'] = false;
}

return $client->get($url, $options)->getBody();
}

public function post($url, $body, array $headers = []) {
$oidcConfig = $this->config->getSystemValue('user_oidc', []);
$client = $this->clientService->newClient();

return $client->post($url, [
$options = [
'headers' => $headers,
'body' => $body,
])->getBody();
];

if (isset($oidcConfig['httpclient.allowselfsigned'])
&& !in_array($oidcConfig['httpclient.allowselfsigned'], [false, 'false', 0, '0'], true)) {
$options['verify'] = false;
}

return $client->post($url, $options)->getBody();
}
}
15 changes: 6 additions & 9 deletions lib/Service/DiscoveryService.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@
namespace OCA\UserOIDC\Service;

use OCA\UserOIDC\Db\Provider;
use OCA\UserOIDC\Helper\HttpClientHelper;
use OCA\UserOIDC\Vendor\Firebase\JWT\JWK;
use OCA\UserOIDC\Vendor\Firebase\JWT\JWT;
use OCP\Http\Client\IClientService;
use OCP\ICache;
use OCP\ICacheFactory;
use Psr\Log\LoggerInterface;
Expand All @@ -39,7 +39,7 @@ class DiscoveryService {

public function __construct(
private LoggerInterface $logger,
private IClientService $clientService,
private HttpClientHelper $clientService,
private ProviderService $providerService,
ICacheFactory $cacheFactory,
) {
Expand All @@ -53,9 +53,7 @@ public function obtainDiscovery(Provider $provider): array {
$url = $provider->getDiscoveryEndpoint();
$this->logger->debug('Obtaining discovery endpoint: ' . $url);

$client = $this->clientService->newClient();
$response = $client->get($url);
$cachedDiscovery = $response->getBody();
$cachedDiscovery = $this->clientService->get($url);

$this->cache->set($cacheKey, $cachedDiscovery, self::INVALIDATE_DISCOVERY_CACHE_AFTER_SECONDS);
}
Expand All @@ -77,8 +75,7 @@ public function obtainJWK(Provider $provider, string $tokenToDecode, bool $useCa
$rawJwks = json_decode($rawJwks, true);
} else {
$discovery = $this->obtainDiscovery($provider);
$client = $this->clientService->newClient();
$responseBody = $client->get($discovery['jwks_uri'])->getBody();
$responseBody = $this->clientService->get($discovery['jwks_uri']);
$rawJwks = json_decode($responseBody, true);
// cache jwks
$this->providerService->setSetting($provider->getId(), ProviderService::SETTING_JWKS_CACHE, $responseBody);
Expand All @@ -99,8 +96,8 @@ public function obtainJWK(Provider $provider, string $tokenToDecode, bool $useCa
public function buildAuthorizationUrl(string $authorizationEndpoint, array $extraGetParameters = []): string {
$parsedUrl = parse_url($authorizationEndpoint);

$urlWithoutParams =
(isset($parsedUrl['scheme']) ? $parsedUrl['scheme'] . '://' : '')
$urlWithoutParams
= (isset($parsedUrl['scheme']) ? $parsedUrl['scheme'] . '://' : '')
. ($parsedUrl['host'] ?? '')
. (isset($parsedUrl['port']) ? ':' . strval($parsedUrl['port']) : '')
. ($parsedUrl['path'] ?? '');
Expand Down
Loading
Loading