owncloud / pyocclient

ownCloud client library for Python
MIT License
294 stars 115 forks source link

Using pyocclient with an ownCloud server using a SSO (Single Sign-On) #204

Open remjg opened 7 years ago

remjg commented 7 years ago

Hi,

I would like to access to the following ownCloud server: https://ent.clg-marigny.ac-reims.fr/owncloud/

Unfortunately, I'm redirected to the following page for logging in: https://ent.clg-marigny.ac-reims.fr:8443/login?service=https%3A%2F%2Fent.clg-marigny.ac-reims.fr%2Fowncloud%2F%3Fapp%3Denvole_cas%26redirect%3D%2Fowncloud%2Findex.php

Hence, I can't seem to be able to use pyocclient to access this ownCloud server since it uses a single sign-on method for logging in.

Is it possible to use pyocclient with an ownCloud server in case the server uses a different authentication method?

PVince81 commented 7 years ago

If you're using OC >= 9.1, login as the user in question then go to the personal page and generate an application token (aka device token). Then use the username as user id and that token as password.

I don't think there is any other way.

remjg commented 7 years ago

The server is running ownCloud 9.0.2 unfortunately, too bad...

Thank you for your prompt answer!

remjg commented 7 years ago

I found that my school uses Central Authentication Service (CAS) as a single sign-on service. Thanks to this very well done article, I managed to log in.

1) First, I have to get the hidden form fields needed to log in (CSRF token).

import requests
import lxml.html
import owncloud

SSO = 'https://ent.clg-marigny.ac-reims.fr:8443/login?service=https%3A%2F%2Fent.clg-marigny.ac-reims.fr%2Fowncloud%2F%3Fapp%3Denvole_cas%26redirect%3D%2Fowncloud%2Findex.php'
s = requests.session()
login = s.get(SSO)
login_html = lxml.html.fromstring(login.text)
hidden_inputs = login_html.xpath(r'//form//input[@type="hidden"]')
form = {x.attrib["name"]: x.attrib["value"] for x in hidden_inputs}

Here is the result:

>>> print(form)
{'lt': 'LT-ent.clg-marigny.ac-reims.fr-761c32d0612e5f23bd95daeb44bf31fc95a6310f147fc84a8cfad650', 'service': 'https://ent.clg-marigny.ac-reims.fr/owncloud/?app=envole_cas&redirect=/owncloud/index.php', 'previous_user': ''}

2) Then, I have to fill out the form with my username and password.

form['username'] = 'MyUsername'
form['password'] = 'MyPassword'
response = s.post(SSO, data=form)

Authentication seems to work since I'm redirected to the correct url:

>>> print(response.url)
https://ent.clg-marigny.ac-reims.fr/owncloud/index.php/apps/files/

3) Now I'm trying to mimic how the Client.login method works while manually overwriting the oc._session attribute.

oc = owncloud.Client('https://ent.clg-marigny.ac-reims.fr/owncloud/')
oc._session = s
oc._session.verify = oc._verify_certs
oc._session.auth = (MyLogin, MyPassword) # necessary again?
oc._update_capabilities()

But of course, trying any command like oc.mkdir('testdir') doesn't work (HTTP error: 401). I'm not surprised since I'm not understanding half of what I'm doing...

Do you have any idea or suggestions regarding what I'm trying to achieve ?

remjg commented 7 years ago

Finally, I managed to get access to my ownCloud storage using pyocclient!

3) Same step as in previous post, I overwrited oc._session attribute with the one I got in step 1) and I ran the method `_update_capabilities()'.

oc = owncloud.Client('https://ent.clg-marigny.ac-reims.fr/owncloud/')
oc._session = s
oc._update_capabilities()

4) Then, I created a new Session object like in the Client.login() method but without running `_update_capabilities()'.

oc._session = requests.session()
oc._session.verify = oc._verify_certs
oc._session.auth = ('MyUsername', 'MyPassword')

5) And the following commands now work:

#oc._update_capabilities() won't run here and before
oc.mkdir('testdir')
#oc._update_capabilities() works again here and after
oc.put_file('testdir/file','file')
oc.share_file_with_user('testdir/', 'aLocalUser')

Yesterday, I discovered that I could configure ownCloud sync client without encountering any issue. Hence I thought that overwritingoc_session wasn't perhaps necessary after "initializing"... As you can see, I still don't understand what I'm doing but I'm lucky enough to have some code that seems to work.

I'll be very happy if you can give me some clue about what happened here !

Maybe should I close the bug since it is not related directly to pyocclient ?

remjg commented 7 years ago

After a bit of polishing, I now use a home made method called Client.login_cas() in place of Client.login() to create a session on the server:

import lxml.html

def login_cas(self, user_id, password):
    """Authenticate to ownCloud behind CAS (Central Authentication Service).
    This will create a session on the server.

    :param user_id: user id
    :param password: password
    :raises: HTTPResponseError in case an HTTP error status was returned
    """

    # Get the hidden form fields needed to log in (CSRF token)
    self._session = requests.session()
    cas_url = self._session.get(self.url).url # follow redirection
    login = self._session.get(cas_url)
    login_html = lxml.html.fromstring(login.text)
    hidden_inputs = login_html.xpath(r'//form//input[@type="hidden"]')
    form = {x.attrib["name"]: x.attrib["value"] for x in hidden_inputs}

    # Fill out the form with username and password, then connect
    form['username'] = user_id
    form['password'] = password
    response = self._session.post(cas_url, data=form)

    try:
        self._update_capabilities()
    except HTTPResponseError as e:
        self._session.close()
        self._session = None
        raise e

    self._session = requests.session()
    self._session.verify = self._verify_certs
    self._session.auth = (user_id, password)

I seem to be able to perform the basic operation I need but I won't try the unit tests since this is the server of my school. Maybe this will be useful to someone !

PVince81 commented 7 years ago

This sounds like a great achievement, however it doesn't feel right to have to hack around HTML pages to make it work. Ideally a proper API endpoint should be added in the server to allow such authentication in a clean way.

remjg commented 7 years ago

(...) it doesn't feel right to have to hack around HTML pages to make it work.

I agree with you.

Ideally a proper API endpoint should be added in the server to allow such authentication in a clean way.

Didn't know what an API endpoint was before reading your comment (and it is still the case I think). After a quick research, I found a page that seems to describe what you are talking about. But it is beyond my knowledge and I will stop here.

Anyway, thank you for you work on pyocclient, it fits my needs!