Skip to content
Open
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
14 changes: 13 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ Provides user creation and login via one single OpenID Connect provider. Even th
- Group creation
- Automatic redirection from the nextcloud login page to the Identity Provider login page
- WebDAV endpoints `Bearer` and `Basic` authentication
- Optional removal of special characters in UID
- Mapping of multiple names to a single display name
- Mapping for birthdate

## Config

Expand Down Expand Up @@ -57,7 +60,8 @@ $CONFIG = array (

// Attribute map for OIDC response. Available keys are:
// * id: Unique identifier for username
// * name: Full name
// * name: Full name, can be a string or an array of strings (use array in case family_name
// and given_name are received separately from IdP).
// If set to null, existing display name won't be overwritten
// * mail: Email address
// If set to null, existing email address won't be overwritten
Expand All @@ -73,6 +77,8 @@ $CONFIG = array (
// at user login. This may lead to security issues. Use with care.
// This will only be effective if oidc_login_update_avatar is enabled.
// * is_admin: If this value is truthy, the user is added to the admin group (optional)
// * birthdate: Since attribute 'birthdate' is supported from NC version 30 onwards, this attribute
// can be mapped too. Accepted format: YYYY-MM-DD
//
// The attributes in the OIDC response are flattened by adding the nested
// array key as the prefix and an underscore. Thus,
Expand Down Expand Up @@ -106,11 +112,13 @@ $CONFIG = array (
// https://openid.net/specs/openid-connect-core-1_0.html#StandardClaims
//
// note: on Keycloak, OIDC name claim = "${given_name} ${family_name}" or one of them if any is missing
// note: for other IdPs providing given name and family name separately: OIDC name claim = array('family_name', 'given_name')
//
'oidc_login_attributes' => array (
'id' => 'sub',
'name' => 'name',
'mail' => 'email',
'birthdate' => 'birthdate',
'quota' => 'ownCloudQuota',
'home' => 'homeDirectory',
'ldap_uid' => 'uid',
Expand Down Expand Up @@ -193,6 +201,10 @@ $CONFIG = array (
// Enable use of WebDAV via OIDC bearer token.
'oidc_login_webdav_enabled' => false,

// Enable removal of special characters in UID. Removal by converting to URL-safe Base64.
// The default value is false.
'oidc_login_remove_special_characters' => false,

// Enable authentication with user/password for DAV clients that do not
// support token authentication (e.g. DAVx⁵)
'oidc_login_password_authentication' => false,
Expand Down
3 changes: 3 additions & 0 deletions appinfo/info.xml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ Provides user creation and login via one single OpenID Connect provider. Even th
- Group creation
- Automatic redirection from the nextcloud login page to the Identity Provider login page
- WebDAV endpoints `Bearer` and `Basic` authentication
- Optional removal of special characters in UID
- Mapping of multiple names to a single display name
- Mapping for birthdate
]]></description>
<version>3.2.2</version>
<licence>agpl</licence>
Expand Down
59 changes: 57 additions & 2 deletions lib/Service/AttributeMap.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,18 @@ class AttributeMap
/** Unique identifier for username */
private string $_id;

/** Full display name of user */
/** Display name of user */
private string $_name;

/** Full display name of user (optional) */
private ?array $_full_name = null;

/** Email address (no overwrite if null) */
private string $_mail;

/** Birthdate (optional) */
private ?string $_birthdate = null;

/** Usage quota for user */
private string $_quota;

Expand All @@ -36,13 +42,17 @@ class AttributeMap
/** If this value is truthy, the user is added to the admin group (optional) */
private ?string $_isAdmin = null;

private IConfig $config;

public function __construct(IConfig $config)
{
$this->config = $config;
$confattr = $config->getSystemValue('oidc_login_attributes', []);
$defattr = [
'id' => 'sub',
'name' => 'name',
'mail' => 'email',
'birthdate' => 'birthdate',
'quota' => 'ownCloudQuota',
'home' => 'homeDirectory',
'ldap_uid' => 'uid',
Expand All @@ -53,7 +63,6 @@ public function __construct(IConfig $config)
$attr = array_merge($defattr, $confattr);

$this->_id = $attr['id'];
$this->_name = $attr['name'];
$this->_mail = $attr['mail'];
$this->_quota = $attr['quota'];
$this->_home = $attr['home'];
Expand All @@ -62,17 +71,31 @@ public function __construct(IConfig $config)
$this->_login_filter = $attr['login_filter'];
$this->_photoUrl = $attr['photoURL'];

if (\is_array($attr['name'])) {
$this->_full_name = $attr['name'];
} else {
$this->_name = $attr['name'];
}

// Optional attributes
if (\array_key_exists('is_admin', $attr)) {
$this->_isAdmin = $attr['is_admin'];
}

if (\array_key_exists('birthdate', $attr)) {
$this->_birthdate = $attr['birthdate'];
}
}

/**
* Get ID from profile.
*/
public function id(array $profile): ?string
{
if (true === $this->config->getSystemValue('oidc_login_remove_special_characters', false)) {
return self::base64url_encode(self::get($this->_id, $profile));
}

return self::get($this->_id, $profile);
}

Expand All @@ -81,6 +104,10 @@ public function id(array $profile): ?string
*/
public function name(array $profile): ?string
{
if (null !== $this->_full_name) {
return self::getFullDisplayName($this->_full_name, $profile);
}

return self::get($this->_name, $profile);
}

Expand All @@ -92,6 +119,14 @@ public function mail(array $profile): ?string
return self::get($this->_mail, $profile);
}

/**
* Get birthdate from profile.
*/
public function birthdate(array $profile): ?string
{
return self::get($this->_birthdate, $profile);
}

/**
* Get quota from profile.
*/
Expand Down Expand Up @@ -186,6 +221,16 @@ public function managesAdmin(): bool
return null !== $this->_isAdmin;
}

/**
* Function to remove unallowed characters.
*
* @param mixed $data
*/
private static function base64url_encode($data): string
{
return rtrim(strtr(base64_encode($data), '+/', '-_'), '=');
}

private static function get(string $attr, array $profile)
{
if (null !== $attr && \array_key_exists($attr, $profile)) {
Expand All @@ -194,4 +239,14 @@ private static function get(string $attr, array $profile)

return null;
}

private static function getFullDisplayName(array|string $attr, array $profile): string
{
$nameArr = [];
foreach ($attr as $value) {
$nameArr[] = self::get($value, $profile);
}

return implode(' ', $nameArr);
}
}
51 changes: 50 additions & 1 deletion lib/Service/LoginService.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
use OC\User\Session;
use OCA\OIDCLogin\Provider\OpenIDConnectClient;
use OCA\User_LDAP\IUserLDAP;
use OCP\Accounts\IAccountManager;
use OCP\IAvatarManager;
use OCP\IConfig;
use OCP\IGroupManager;
Expand All @@ -22,6 +23,7 @@ class LoginService
{
public const USER_AGENT = 'NextcloudOIDCLogin';

private IAccountManager $accountManager;
private IAvatarManager $avatarManager;
private IConfig $config;
private IRequest $request;
Expand All @@ -44,7 +46,8 @@ public function __construct(
IL10N $l,
IProvider $tokenProvider,
LoggerInterface $logger,
AttributeMap $attr
AttributeMap $attr,
IAccountManager $accountManager,
) {
$this->config = $config;
$this->request = $request;
Expand All @@ -55,6 +58,7 @@ public function __construct(
$this->tokenProvider = $tokenProvider;
$this->logger = $logger;
$this->attr = $attr;
$this->accountManager = $accountManager;

// get external storage service if available
$this->storagesService = class_exists('\OCA\Files_External\Service\GlobalStoragesService')
Expand Down Expand Up @@ -369,6 +373,23 @@ private function updateBasicProfile(IUser $user, array $profile): void
}
}

// Set Birthdate
if (null !== ($birthdate = $this->attr->birthdate($profile))) {
$validateBirthdate = $this->validateBirthdate($birthdate);
if (null !== $validateBirthdate) {
$account = $this->accountManager->getAccount($user);
$account->setProperty(
IAccountManager::PROPERTY_BIRTHDATE,
$validateBirthdate,
IAccountManager::SCOPE_LOCAL,
IAccountManager::NOT_VERIFIED
);
$this->accountManager->updateAccount($account);
} else {
$this->logger->debug("Skipping invalid birthdate for user: {$user->getUID()}");
}
}

// Set quota
if (null !== ($quota = $this->attr->quota($profile))) {
$user->setQuota((string) $quota);
Expand Down Expand Up @@ -533,4 +554,32 @@ private function flatten(array $array, string $prefix = ''): array

return $result;
}

/**
* Validate and normalize birthdate according to OIDC spec.
* Only accepts full dates in YYYY-MM-DD format.
*/
private function validateBirthdate(string $birthdate): ?string
{
$birthdate = trim($birthdate);

if (empty($birthdate)) {
return null;
}

if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $birthdate)) {
$this->logger->debug("Birthdate must be in YYYY-MM-DD format, got: {$birthdate}");

return null;
}

$date = \DateTime::createFromFormat('Y-m-d', $birthdate);
if (!$date || $date->format('Y-m-d') !== $birthdate) {
$this->logger->debug("Invalid birthdate value: {$birthdate}");

return null;
}

return $birthdate;
}
}