Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement OAuth2 provider #927

Merged
merged 15 commits into from Jun 5, 2018
4 changes: 2 additions & 2 deletions .travis.sh
Expand Up @@ -39,11 +39,11 @@ if [ "$1" == "translation-spelling" ]; then
potypo
fi
if [ "$1" == "tests" ]; then
pip3 install -r src/requirements.txt -Ur src/requirements/dev.txt -r src/requirements/py34.txt
pip3 install -r src/requirements.txt -Ur src/requirements/dev.txt -r src/requirements/py34.txt pytest-xdist
cd src
python manage.py check
make all compress
py.test --reruns 5 tests
py.test --reruns 5 -n 2 tests
fi
if [ "$1" == "tests-cov" ]; then
pip3 install -r src/requirements.txt -Ur src/requirements/dev.txt -r src/requirements/py34.txt
Expand Down
2 changes: 1 addition & 1 deletion .travis.yml
Expand Up @@ -15,7 +15,7 @@ matrix:
- python: 3.6
env: JOB=tests PRETIX_CONFIG_FILE=tests/travis_sqlite.cfg
- python: 3.6
env: JOB=tests-cov
env: JOB=tests-cov PRETIX_CONFIG_FILE=tests/travis_postgres.cfg
- python: 3.6
env: JOB=style
- python: 3.4
Expand Down
40 changes: 22 additions & 18 deletions doc/api/fundamentals.rst
Expand Up @@ -6,27 +6,13 @@ with pretix' REST API, such as authentication, pagination and similar definition

.. _`rest-auth`:

Obtaining an API token
----------------------

To authenticate your API requests, you need to obtain an API token. You can create a
token in the pretix web interface on the level of organizer teams. Create a new team
or choose an existing team that has the level of permissions the token should have and
create a new token using the form below the list of team members:

.. image:: img/token_form.png
:class: screenshot

You can enter a description for the token to distinguish from other tokens later on.
Once you click "Add", you will be provided with an API token in the success message.
Copy this token, as you won't be able to retrieve it again.

.. image:: img/token_success.png
:class: screenshot

Authentication
--------------

If you're building an application for end users, we strongly recommend that you use our
:ref:`OAuth-based authentication progress <rest-oauth>`. However, for simpler needs, you
can also go with static API tokens that you can create on a per-team basis (see below).

You need to include the API token with every request to pretix' API in the ``Authorization`` header
like the following:

Expand All @@ -44,6 +30,24 @@ like the following:
adding OAuth2 support in the future for user-level authentication. If you want
to use session authentication, be sure to comply with Django's `CSRF policies`_.

Obtaining an API token
----------------------

To authenticate your API requests, you need to obtain an API token. You can create a
token in the pretix web interface on the level of organizer teams. Create a new team
or choose an existing team that has the level of permissions the token should have and
create a new token using the form below the list of team members:

.. image:: img/token_form.png
:class: screenshot

You can enter a description for the token to distinguish from other tokens later on.
Once you click "Add", you will be provided with an API token in the success message.
Copy this token, as you won't be able to retrieve it again.

.. image:: img/token_success.png
:class: screenshot

Permissions
-----------

Expand Down
1 change: 1 addition & 0 deletions doc/api/index.rst
Expand Up @@ -14,4 +14,5 @@ in functionality over time.
:maxdepth: 2

fundamentals
oauth
resources/index
171 changes: 171 additions & 0 deletions doc/api/oauth.rst
@@ -0,0 +1,171 @@
.. _`rest-oauth`:

OAuth support / "Connect with pretix"
=====================================

In addition to static tokens, pretix supports `OAuth2`_-based authentication starting with
pretix 1.16. This allows you to put a "Connect with pretix" button into your website or tool
that allows the user to easily set up a connection between the two systems.

If you haven't worked with OAuth before, have a look at the `OAuth2 Simplified`_ tutorial.

Registering an application
--------------------------

To use OAuth, you need to register your application with the pretix instance you want to connect to.
In order to do this, log in to your pretix account and go to your user settings. Click on "Authorized applications"
first and then on "Manage your own apps". From there, you can "Create a new application".

You should fill in a descriptive name of your application that allows users to recognize who you are. You also need to
give a list of fully-qualified URLs that users will be redirected to after a successful authorization. After you pressed
"Save", you will be presented with a client ID and a client secret. Please note them down and treat the client secret
like a password; it should not become available to your users.

Obtaining an authorization grant
--------------------------------

To authorize a new user, link or redirect them to the ``authorize`` endpoint, passing your client ID as a query
parameter. Additionally, you can pass a scope (currently either ``read``, ``write``, or ``read write``)
and an URL the user should be redirected to after successful or failed authorization. You also need to pass the
``response_type`` parameter with a value of ``code``. Example::

https://pretix.eu/api/v1/oauth/authorize?client_id=lsLi0hNL0vk53mEdYjNJxHUn1PcO1R6wVg81dLNT&response_type=code&scope=read+write&redirect_uri=https://pretalx.com

To prevent CSRF attacks, you can also optionally pass a ``state`` parameter with a random string. Later, when
redirecting back to your application, we will pass the same ``state`` parameter back to you, so you can compare if they
match.

After the user granted or denied access, they will be redirected back either to the ``redirect_url`` you passed in the
query or to the first redirect URL configured in your application settings.

On successful registration, we will append the query parameter ``code`` to the URL containing an authorization code.
For example, we might redirect the user to this URL::

https://pretalx.com/?code=eYBBf8gmeD4E01HLoj0XflqO4Lg3Cw&state=e3KCh9mfx07qxU4bRpXk

You will need this ``code`` parameter to perform the next step.

On a failed registration, a query string like ``?error=access_denied`` will be appended to the redirection URL.

.. note:: In this step, the user is allowed to restrict your access to certain organizer accounts. If you try to
re-authenticate the user later, the user might be instantly redirected back to you if authorization is already
given and would therefore be unable to review their organizer restriction settings. You can append the
``approval_prompt=force`` query parameter if you want to make sure the user actively needs to confirm the
authorization.

Getting an access token
-----------------------

Using the ``code`` value you obtained above and your client ID, you can now request an access token that actually gives
access to the API. The ``token`` endpoint expects you to authenticate using `HTTP Basic authentication`_ using your client
ID as a username and your client secret as a password. You are also required to again supply the same ``redirect_uri``
parameter that you used for the authorization.

.. http:get:: /api/v1/oauth/token

Request a new access token

**Example request**:

.. sourcecode:: http

POST /api/v1/oauth/token HTTP/1.1
Host: pretix.eu
Accept: application/json, text/javascript
Authorization: Basic bHNMaTBoTkwwdms1M21FZFlqTkp4SFVuMVBjTzFSNndWZzgxZExOVDplSmpzZVA0UjJMN0hMcjBiS0p1b3BmbnJtT2cyY3NDeTdYaFVVZ0FoalhUU0NhZHhRTjk3cVNvMkpPaXlWTFpQOEozaTVQd1FVdFIwNUNycG5ac2Z0bXJjdmNTbkZ1SkFmb2ZsUTdZUDRpSjZNTWFYTHIwQ0FpNlhIRFJjV1Awcg==

grant_type=authorization_code&code=eYBBf8gmeD4E01HLoj0XflqO4Lg3Cw&redirect_uri=https://pretalx.com

**Example response**:

.. sourcecode:: http

HTTP/1.1 200 OK
Vary: Accept
Content-Type: application/json

{
"access_token": "i3ytqTSRWsKp16fqjekHXa4tdM4qNC",
"expires_in": 86400,
"token_type": "Bearer",
"scope": "read write",
"refresh_token": "XBK0r8z4A4TTeR9LyMUyU2AM5rqpXp"
}

:statuscode 200: no error
:statuscode 401: Authentication failure


As you can see, you receive two types of tokens: One "access token", and one "refresh token". The access token is valid
for a day and can be used to actually access the API. The refresh token does not have an expiration date and can be used
to obtain a new access_token after a day, so you should make sure to store the access token safely if you need long-term
access.

Using the API with an access token
----------------------------------

You can supply a valid access token as a ``Bearer``-type token in the ``Authorization`` header to get API access.

.. sourcecode:: http
:emphasize-lines: 3

GET /api/v1/organizers/ HTTP/1.1
Host: pretix.eu
Authorization: Bearer i3ytqTSRWsKp16fqjekHXa4tdM4qNC

Refreshing an access token
--------------------------

You can obtain a new access token using your refresh token any time. This can be done using the same ``token`` endpoint
used to obtain the first access token above, but with a different set of parameters:

.. sourcecode:: http

POST /api/v1/oauth/token HTTP/1.1
Host: pretix.eu
Accept: application/json, text/javascript
Authorization: Basic bHNMaTBoTkwwdms1M21FZFlqTkp4SFVuMVBjTzFSNndWZzgxZExOVDplSmpzZVA0UjJMN0hMcjBiS0p1b3BmbnJtT2cyY3NDeTdYaFVVZ0FoalhUU0NhZHhRTjk3cVNvMkpPaXlWTFpQOEozaTVQd1FVdFIwNUNycG5ac2Z0bXJjdmNTbkZ1SkFmb2ZsUTdZUDRpSjZNTWFYTHIwQ0FpNlhIRFJjV1Awcg==

grant_type=refresh_token&refresh_token=XBK0r8z4A4TTeR9LyMUyU2AM5rqpXp

The previous access token will instantly become invalid.

Revoking a token
----------------

If you don't need a token any more or if you believe it may have been compromised, you can use the ``revoke_token``
endpoint to revoke it.

.. http:get:: /api/v1/oauth/revoke_token

Revoke an access or refresh token. If you revoke an access token, you can still create a new one using the refresh token. If you
revoke a refresh token, the connected access token will also be revoked.

**Example request**:

.. sourcecode:: http

POST /api/v1/oauth/revoke_token HTTP/1.1
Host: pretix.eu
Accept: application/json, text/javascript
Authorization: Basic bHNMaTBoTkwwdms1M21FZFlqTkp4SFVuMVBjTzFSNndWZzgxZExOVDplSmpzZVA0UjJMN0hMcjBiS0p1b3BmbnJtT2cyY3NDeTdYaFVVZ0FoalhUU0NhZHhRTjk3cVNvMkpPaXlWTFpQOEozaTVQd1FVdFIwNUNycG5ac2Z0bXJjdmNTbkZ1SkFmb2ZsUTdZUDRpSjZNTWFYTHIwQ0FpNlhIRFJjV1Awcg==

token=XBK0r8z4A4TTeR9LyMUyU2AM5rqpXp

**Example response**:

.. sourcecode:: http

HTTP/1.1 200 OK
Vary: Accept
Content-Type: application/json

:statuscode 200: no error
:statuscode 401: Authentication failure

If you want to revoke your client secret, you can generate a new one in the list of your managed applications in the
pretix user interface.

.. _OAuth2: https://en.wikipedia.org/wiki/OAuth
.. _OAuth2 Simplified: https://aaronparecki.com/oauth-2-simplified/
.. _HTTP Basic authentication: https://en.wikipedia.org/wiki/Basic_access_authentication
9 changes: 9 additions & 0 deletions src/pretix/api/__init__.py
@@ -0,0 +1,9 @@
from django.apps import AppConfig


class PretixApiConfig(AppConfig):
name = 'pretix.api'
label = 'pretixapi'


default_app_config = 'pretix.api.PretixApiConfig'
10 changes: 10 additions & 0 deletions src/pretix/api/auth/permission.py
@@ -1,5 +1,6 @@
from rest_framework.permissions import SAFE_METHODS, BasePermission

from pretix.api.models import OAuthAccessToken
from pretix.base.models import Event
from pretix.base.models.organizer import Organizer, TeamAPIToken
from pretix.helpers.security import (
Expand Down Expand Up @@ -55,6 +56,15 @@ def has_permission(self, request, view):

if required_permission and required_permission not in request.orgapermset:
return False

if isinstance(request.auth, OAuthAccessToken):
if not request.auth.allow_scopes(['write']) and request.method not in SAFE_METHODS:
return False
if not request.auth.allow_scopes(['read']) and request.method in SAFE_METHODS:
return False
if isinstance(request.auth, OAuthAccessToken) and hasattr(request, 'organizer'):
if not request.auth.organizers.filter(pk=request.organizer.pk).exists():
return False
return True


Expand Down