How to work the OAuth2 with Nextcloud?

Hi everyone :slight_smile:

I would like to know how to work the OAuth2 with Nextcloud ?

My use case is to use a API/REST on the backend (PHP) and not on the frontend.

But, I have problems when it comes to SSO users. In fact, I cannot get their credentials in the authentication part with the Basic type.

So, maybe the OAuth2 can resolve my problems (?)

Of course, I read this documentation : OAuth2 — Nextcloud latest Administration Manual latest documentation .

I defined the rederict_uri and the name. But, I don’t know what I should do next.

Someone can help me, please ?

Just to be sure, we are on the same page:

You want to authenticate a 3rd party website using OAuth2? What do you mean by on the backend? The authentication process itself to obtain a token or the usage of the tokens in a request triggered by the backend?

Hmmm… Not I don’t want to authenticate a 3rd party website using OAuth2.
I give you the context.

I work on the Workspace app and it uses the Groupfolders app.
You can see Workspace app as a plugin for the Groupfolders app.

With @StCyr , we interacted with Groupfolders via its API/REST the back-end side.

But, we have encountered problems to use those API/REST with SSO users.
So, to resolve this problem, we migrated all API/REST calls from Groupfolders to the front-end.

For example in the old Pull-Request : Refactor : All Groupfolders' APIs call rewritten front-end side by zak39 · Pull Request #376 · arawa/workspace · GitHub .

But now, my code is very dirty… For example, when I create a workspace :

  1. When one user creates a workspace, the javascript part calls the API/REST Groupfolders to create a groupfolder and it returns the folderId.

  2. With the folderId, I use my API/REST to create a workspace and I specify the folderId and the space name and I get an object as Json.

  3. With the folderId I got in the step 1. , I used to get a groupfolder via its API/REST and I will merge its result with the result from the step 2..

  4. Then, I get a format of users and groups to complete my data structured.

It’s really not clean to call API/REST and build my data structure on the front-end and it’s difficult to maintain.

Now, I would like to migrate all the Groupfolders API/REST calls that are on the JavaScript side to the back-end.

So, I think the OAuth2 could help me do API/REST authentication, even for SSO users (?) :thinking:
I don’t know…

@carlschwan thought I can create an interface for the FolderManager but I don’t know what to do about the dependency injection part…

I don’t know if my explanations are clear ? ^^’

OK, I think I get your problem.

Unfortunaely, I doubt that OAuth2 will help you much here. The NC implementation only allows to authenticate users: third party apps can request and check the validity of a user by means of the question if this user is part of a NC instance. Similar to the common login via google/github/facebook/… on many pages. AFAIK the implementation does not provide authentication, so you cannot use these to login in NC itself. There is another app to provide this but this is a separate issue/topic.

In fact, you are facing an inter-app communication problem. I asked a few months ago a similar question: Integration of apps - best practices.

Regarding the dependency injection issue:
You should be able to

use OCA\GroupFolders\Folder\FolderManager

This is independent of the class is present or not. Then you can do

if (class_exists(FolderManager::class)) { /* ... */ }

You must not use the class unless you know it is reachable by PHP. So you must not simply write a class with the appropriate constructor but you will ost probably have to write the same class twice (like a wrapper) and throw an exception in the case the class was not found. Here is an untested code snippet to get the idea:

if (class_exists(FolderManager::class) {
  class FolderManagerWrapper {
    public function __construct(FolderManager(FolderManager $fm) {
      $this->fm = $fm;
    }
    public function getManager() {
      return $this->fm;
    }
  }
} else {
  class FolderManagerWrapper {
    public function __construct() {
    }
    public function getManager() {
      throw new Exception('The Groupfolder app was not installed');
    }
  }
}

As I said, I have not tested it yet. So you might need to tweak a bit.
Is this what you where looking for?

1 Like

That’s what I thought… It’s a really important question to know how to integrate apps into other apps.

Oooh nice topic :+1:

I never seen it until now !

I’m glad to hear about it. I thought I was alone on this problem ><

It’s okay, I think I understand your idea. You advise me to create a wrapper and not use FolderManager directly…

Hmmm… I will see the code tomorrow, but the idea is this :

<?php

class FolderManagerWrapper() {
  private static $instance = null;

  private function __construct(private FolderManager $fm) {   
  }

 public function getInstance(): FolderManagerWrapper {
   if (!class_exists(FolderManager::class)) {
       throw new Exception("The FolderManager class does not exist.");
   }
   if (is_null(self::$instance) {
     self::instance = new FolderManagerWrapper() // I should inject its dependencies here
   }

   return self::$instance;
 }
}

It’s a singleton, so I will test it tomorrow. It’s the way I see it.

But, in your idea, I use the FolderManager “directly”. However, it’s not a best practice for Nextcloud.
I told about it with Christoph Wurst on the Collective app : Nextcloud .

The collective app depends directly Circles, but I don’t understand why we have apps that make exceptions :thinking:
All that to say, if I use FolderManager, even as a wrapper, am I in the same case than the Collective App ?
Do I respect the Nextcloud’s rules ?

Edit - 12/07/2023 at 11.57pm

I tested this code and it works !

<?php

$isActive = false;

if ($isActive) {
	class A {
	}
} else {
	die("Error is not active");
}

$a = new A();

var_dump($a);

This is not 100% going to work. The problem is that your service class will have a dependency injection on the class (A in the case). This will work if the class is to be found ($isActive == true`) but fail otherwise.
The idea was to provide two class implementations, one with the wrapper functional and one with a default implementation that does not depend on third party app code. With the check you select which implementation should be used. The default implementation will/must therefore not depend on the third party class in any way (dependency injection in the constructor or any other use case).

You could also do a similar pattern as the server where you define an interface (IA) and multiple classes that implement the interface (AWrapped and ADefault). Both classes AWrapped and ADefault can be implemented as usual to use the foreign class A (AWrapped) or throw a default exception. You can then in your Application class register either class as an implementation of IA depending on the existence of class A (see the docs on the manual dependency injection).

Yeah, accessing foreign classes is brittle and tends to be more work. Also you might need to keep a list of compatible version combinations of the involved apps. Not nice.

The thing is that you would need support by the other app in order to do other things. I personally think the event system to be a good way but then the groupfolder app needs to provide listeners and events to trigger things.

My understanding is that they wanted to have Circles rolled out quickly and did not touch the code anymore since (in a breaking way at least). So, there was some way of public API classes defined that should not change. (This is something you could try to negotiate with the groupfolder team as well) In the meanwhile it is part of various documentation and tutorials and will not be changed in the near future/a quick way.

I think you have to distinguish between rules and best practices.

  • Rules are to prohibit bad behavior, like removing all data of users whose name start with a r. (You have a grunge with people with names starting with r due to bad experience with a foreign girlfriend. So you keep getting revenge. :smiling_imp:) This is a broken rule and will get your app banned.
  • Best-practices should help you with the work. It shows how things are intended to be used and done. You are not obligated to respect these. Disrespecting them makes work harder for you and your fellows.
    Sometimes, though, some use cases are not considered in the initial consideration and will not work within the best-practices. Then, you are asked to find a minimal invasive solution that sticks as close as possible in order to be maintainable. Keep it well documented and communicate it clearly.

Yeah, I know, it’s just a little test ^^

I understand the idea… It could work !
Normally, the Nextcloud team would not come to me to say that isn’t a best practice and blacklist my app in the store ?

I think it’s a good idea to use this pattern type :+1:
I will study the question :slight_smile:

I never use the Event and Listener system until now ^^’
So, I cannot tell whether it’s a good idea or not, I trust you :slight_smile:

I don’t know for the Circles app. But, they have control over their software (server and theirs apps), so I think they can make exceptions with a few apps (?)

Yes, thank you for this distinction :pray:

With a funny example :smile:

But that’s precisely the point, Nextcloud didn’t define a rule (not a best practice) that you don’t touch \OCA namespaces ? Because, they consider those namespaces as a private namespace (like the \OC namespace) ?

I seem to have read it (?) :thinking:

1 Like

Blacklisting for sure not. If there is e.g. a security issue, maybe but they will come to you before. They want to have support by the community and why shy away their supporters (devs)?

I haven’t myself. I just see it as an option to decouple things (see publish subscribe pattern).

Yes, sort of private namespaces. I have nowhere read it formally written to be discouraged “only” in the forums and discussions (considered second-hand information).
Also the \OC namespace is not private but just not guaranteed to be stable. So your app would need much more version checks and more complicated code than necessary.
And as you say, the Circles app uses this approach officially. The same holds true with the calendars and the dav app (although there are changes on the horizon). So, I would not consider this a problem in the near future. If this would be banned, they need to provide better mechanisms until then.

1 Like

Hi @christianlupus :smiley:

I don’t know, I take precautions for my company :no_mouth:

Yeah, it’s a “pattern” I would like to learn ^^
Thanks for the wiki page :slight_smile:

I followed your instructions and I succeed !

I tried this code and it works ! :partying_face:

<?php

declare(strict_types=1);

namespace OCA\Workspace\Groupfolders;

use Exception;
use OCA\GroupFolders\Folder\FolderManager;
use OCA\GroupFolders\Service\DelegationService;
use OCP\AppFramework\Http\DataResponse;
use OCP\AppFramework\QueryException;
use OCP\Files\IRootFolder;
use OCP\Server;
use Psr\Container\ContainerInterface;

if (class_exists(FolderManager::class)) {
    class Wrapper {
        private FolderManager $fm;
        private DelegationService $delegationServiceGroupFolders;
    
        public function __construct(private IRootFolder $rootFolder)
        {
            try {
                // $this->fm = $appContainer->get(FolderManager::class);
                $this->fm = Server::get(FolderManager::class);
                $this->delegationServiceGroupFolders = Server::get(DelegationService::class);
            } catch (Exception $e) {
                throw new \Exception($e->getMessage());
            }
        }
    
        public function createFolder(string $mountpoint): int
        {
            return $this->fm->createFolder($mountpoint);
        }

        public function setFolderACL(int $folderId, bool $acl): void
        {
            $this->fm->setFolderACL($folderId, $acl);
        }

        public function addApplicableGroup(int $id, string $group): void
        {
            $this->fm->addApplicableGroup($id, $group);
        }

        public function setManageACL(int $folderId, string $type, string $id, bool $manageAcl): void
        {
            $this->fm->setManageACL($folderId, $type, $id, $manageAcl);
        }

        public function getFolder(int $id, int $rootStorageId): array
        {
            return $this->fm->getFolder($id, $rootStorageId);
        }

        private function formatFolder(array $folder): array {
            $folder['group_details'] = $folder['groups'];
            $folder['groups'] = array_map(function (array $group) {
                return $group['permissions'];
            }, $folder['groups']);

            return $folder;
        }

        public function getAllFoldersWithSize(int $rootStorageId): array {
            return $this->fm->getAllFoldersWithSize($rootStorageId);
        }

        private function getRootFolderStorageId(): ?int
        {
            return $this->rootFolder->getMountPoint()->getNumericStorageId();
        }
    }
} else {
    class Wrapper {
        private const MESSAGE = 'The groupfolder app was installed.';
        
        public function __construct()
        {
        }
        
        public function createFolder(string $mountpoint): int {
            throw new \Exception(self::MESSAGE);
            return 100;
        }

        public function setFolderACL(int $folderId, bool $acl): void {
            throw new \Exception(self::MESSAGE);
        }

        public function addApplicableGroup(int $id, string $group): void
        {
            throw new \Exception(self::MESSAGE);
        }

        public function setManageACL(int $folderId, string $type, string $id, bool $manageAcl): void
        {
            throw new \Exception(self::MESSAGE);
        }

        public function getFolder(int $id, int $rootStorageId): array
        {
            throw new \Exception(self::MESSAGE);
            return [];
        }
    }
}

I created a experiment branch on my remote and you can explore the code, if you want :slight_smile:

The Wrapper.php on the GitHub :point_right: workspace/lib/Groupfolders/Wrapper.php at experiment/call-apigroupfolders-in-backend · arawa/workspace · GitHub

1 Like

Do you run into this exception? If not (I am pretty sure, you will not if you have the outer if in place), you can just use it in the constructor arguments as you did with the IRootFolder.

Keep me posted on your findings.

1 Like

No, I don’t encounter this exception and I confirm you it works if I declare the $fm attribute in the constructor :

<?php

declare(strict_types=1);

namespace OCA\Workspace\Groupfolders;

use Exception;
use OCA\GroupFolders\Folder\FolderManager;
use OCA\GroupFolders\Service\DelegationService;
use OCP\AppFramework\Http\DataResponse;
use OCP\AppFramework\QueryException;
use OCP\Files\IRootFolder;
use OCP\Server;
use Psr\Container\ContainerInterface;

if (class_exists(FolderManager::class)) {
    class Wrapper {
        // private FolderManager $fm;
        private DelegationService $delegationServiceGroupFolders;
    
        public function __construct(private IRootFolder $rootFolder, private FolderManager $fm)
        {
            // try {
            //     // $this->fm = $appContainer->get(FolderManager::class);
            //     $this->fm = Server::get(FolderManager::class);
            //     $this->delegationServiceGroupFolders = Server::get(DelegationService::class);
            // } catch (Exception $e) {
            //     throw new \Exception($e->getMessage());
            // }
        }
    
        public function createFolder(string $mountpoint): int
        {
            return $this->fm->createFolder($mountpoint);
        }

        public function setFolderACL(int $folderId, bool $acl): void
        {
            $this->fm->setFolderACL($folderId, $acl);
        }

        public function addApplicableGroup(int $id, string $group): void
        {
            $this->fm->addApplicableGroup($id, $group);
        }

        public function setManageACL(int $folderId, string $type, string $id, bool $manageAcl): void
        {
            $this->fm->setManageACL($folderId, $type, $id, $manageAcl);
        }

        public function getFolder(int $id, int $rootStorageId): array
        {
            return $this->fm->getFolder($id, $rootStorageId);
        }

        private function formatFolder(array $folder): array {
            $folder['group_details'] = $folder['groups'];
            $folder['groups'] = array_map(function (array $group) {
                return $group['permissions'];
            }, $folder['groups']);

            return $folder;
        }

        public function getAllFoldersWithSize(int $rootStorageId): array {
            return $this->fm->getAllFoldersWithSize($rootStorageId);
        }

        private function getRootFolderStorageId(): ?int
        {
            return $this->rootFolder->getMountPoint()->getNumericStorageId();
        }
    }
} else {
    class Wrapper {
        private const MESSAGE = 'The groupfolder app was installed.';
        
        public function __construct()
        {
        }
        
        public function createFolder(string $mountpoint): int {
            throw new \Exception(self::MESSAGE);
            return 100;
        }

        public function setFolderACL(int $folderId, bool $acl): void {
            throw new \Exception(self::MESSAGE);
        }

        public function addApplicableGroup(int $id, string $group): void
        {
            throw new \Exception(self::MESSAGE);
        }

        public function setManageACL(int $folderId, string $type, string $id, bool $manageAcl): void
        {
            throw new \Exception(self::MESSAGE);
        }

        public function getFolder(int $id, int $rootStorageId): array
        {
            throw new \Exception(self::MESSAGE);
            return [];
        }
    }
}

So, I see it works and do you think I can use this code to manipulate Groupfolders even if it’s not good practice ?

I would contact the author of the group folder app to clarify this. He/she should be aware of the problem and your solution so far.

I suggest to establish a way to communicate by events as these seem less brittle. Eventually, you can get him/her to define a NC-public API (in the server like e.g. Talk) but this is only possible in the long term. I would

  • go with the direct import in the short time scale to test things and make sure it works in general as expected.
  • Establish the event system in the mid time scale to be more robust and also define some sort of stable API
  • In the long run, you might to convince the author to define/fix a global interface (like the currently not existing OCP\Groupfolder\IGroupFolderManager with a public defined API).

Hi @christianlupus :slight_smile:

Thank you very much for you help :pray:
Don’t hesitate if I can help you :slight_smile:

We (Arawa) know Carl but, he left the Nextcloud company :confused:

So, we don’t know who maintains the Groupfolders app :thinking:

I talked with my coworkers and we don’t want to use the direct import even if it’s in the short timescale. Because, we want our app is robust.

So, we would to try the Event / Listener system as a POC for our app.
But, if I understand this logic. That means we or Groupfolders’ developers have to write Events on the Groupfolders apps (GroupFolderCreateEvent, GroupfolderSetACLEvent, and so on. Or even GroupfolderEvent) and in the Workspace app, we have to write the Listeners on those events.
Is that it ?

1 Like

You could just open an issue and see who might react. Alternatively, you can directly try to contact to most active users (icewind1991 and juliuishaertl).

You got it correct.

You could (for the sake of the POC) fork the groupfolder repo and hack in the relevant events. Then you could also present this as a starting point for the real groupfolder team.

Yes, I saw that yesterday and I will see with them if we need to create a Pull Request :slight_smile:

Hmmm… In fact, I wonder if this is a good idea ?

Because, I need to create a groupfolder before create a workspace.

For example :

  1. I call this API/REST with PostMan or Insomnia : POST - apps/workspace/spaces - with the “spacename” as POST variable.

  2. My route redirect the request to the create method from WorkspaceController.

  3. The WorkspaceController#create get the spacename variable

  4. First, I create a groupfolder, then I create a workspace where I define the folder_id (the groupfolder identifier) as an argument to create a relationship between them.

So, I don’t think it’s a good solution, no ?
Because, with the Event and Listener system, I have to create Events on the Workspace side and Listeners on the Groupfolders side, no ?

Edit - 27 july, 2023 at 11.54am

When I see the Collective App, they use a “Helper” and get the CircleManager via the appContainer->get : collectives/lib/Service/CircleHelper.php at 485b84800f71fa70a5b80a86575cf01c6bcc0d32 · nextcloud/collectives · GitHub .

But, I don’t know if it’s a good practice or not (?)

You cannot get it atomic. So you will have to do one after the other.

And yes, the listener must be on the side of the groupfolder app. They could provide the events as well (like e.g. TriggerCreateGroupfolderEvent or similar). Then you could create and dispatch such an event.

You will however still run into the problem that your app will import OCA\Groupfolder classes (the event class!) in your code. So, you are only partially better than just calling the classes directly. This is a design flaw as the events are considered typed and thus you need a common code base. You could only prevent that if you used plain PHP types (strings/arrays/…), I guess.

However, it is better than just calling arbitrary classes as you restrict yourself to the set of methods/events that are actually stable and consistent.

This is in fact very similar to the approach discussed above where you import OCA\Groupfolder classes directly.

I’m sorry, what do you mean by “atomic” ?

I have the impression that using OCA\Groupfolder is fairly general in Nextcloud ?

Because, if using the Events and Listeners system is close to good practice. We have to import a OCA\<appid> in any case.
As you said, it’s a design flaw (?)

However, I succeeded to work one event/listener.

I created my Event in the Workspace app :point_right: workspace/lib/Events/WorkspaceCreateEvent.php at experiment/call-groupfolders-events-listeners · arawa/workspace · GitHub .

I registered my EventListener in the Application.php from Groupfolders :point_right: groupfolders/lib/AppInfo/Application.php at 709dc7da79da07b37210a23ea2fbf29918f82c05 · arawa/groupfolders · GitHub .
I created my Listener from Groupfolders :point_right: groupfolders/lib/Listeners/WorkspaceCreateListener.php at experiment/create-events-groupfolders · arawa/groupfolders · GitHub .

And it’s work ! :muscle:

But…
After I sent my event in my controller :point_right: workspace/lib/Controller/WorkspaceController.php at f29ee654d43f896fe34256cb2990378f03c7f954 · arawa/workspace · GitHub , I would like to get a groupfolder and that’s where it gets tricky…

I think this is where we have our limits in the Event / Listeners system.

Mybay I wrong but, I don’t know how to get a groupfolder from this step (it’s a pseudo code).

Yes, and I understand the Collective’s problem now.

When you said to take the short term solution. Do you think this solution can last 3 years ? 5 years ? Until we (Arawa and the Groupfolder’s developers or Nextcloud) found a solution ?

1 Like

The problem arises even earlier but with your implementation on the groupware side: The need to use/import part of your code in order to understand the received event.

Your code is currently tailored down to your use case. This is fine to provide a POC but will not work 100% for the upstream app. They will not want to integrate a code snippet for every foreign app. But this can be addressed later on.

Most probably, I would say that they would need to deliver an event themselves with the group information (aka listener on your side) and your would get the folder as a payload. It would be sort of a ping-pong game with events.

But as you see, the types events make it necessary to have a common code base.

This can even last longer, I guess unless one app ceases to exist.

I’d say it is absolutely vital to define some classes that are not changed regularly. I am thinking of defining some classes in OCA\Groupfolder\API that provide very generic interfaces and that interfaces must not change much.
Each time, the API classes need to change their interface, you need to be very careful about compatibility. Maybe you should define in the very first moment a way to exchange the compatibility information (like a separate API version that you can trust to be semver-based). You will have to push out a new release once the API changes.

At least the group folder app seems rather stable (not much new features), so the API might be quite stable as well. So, there is a good chance that you will not see yourself in the situation to force out a hot fix release too often.