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

Authentication #109

Merged
merged 12 commits into from
May 24, 2016
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
9 changes: 9 additions & 0 deletions docs/source/_json/login.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
POST /plone/@login
Accept: application/json

HTTP 200 OK
content-type: application/json

{
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJmdWxsbmFtZSI6IiIsInN1YiI6ImFkbWluIn0.SZDnl_baH5M_StJJrzfbj7o-5My30NmSFbMrhpSX5I4"
}
9 changes: 9 additions & 0 deletions docs/source/_json/login_renew.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
POST /plone/@login-renew
Accept: application/json

HTTP 200 OK
content-type: application/json

{
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJmdWxsbmFtZSI6IiIsInN1YiI6ImFkbWluIn0.SZDnl_baH5M_StJJrzfbj7o-5My30NmSFbMrhpSX5I4"
}
5 changes: 5 additions & 0 deletions docs/source/_json/logout.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
POST /plone/@logout
Accept: application/json

HTTP 204 No Content

89 changes: 88 additions & 1 deletion docs/source/authentication.rst
Original file line number Diff line number Diff line change
Expand Up @@ -31,4 +31,91 @@ Or the same example using ``curl``:

.. code:: python

curl -u username:password -H 'Accept:application/json' $URL
curl -u username:password -H 'Accept:application/json' $URL


JSON Web Tokens (JWT)
---------------------

``plone.restapi`` includes a Plone PAS plugin for authentication with JWT. The
plugin is installed automatically when installing the product.

A JWT token can be acquired by posting a users credentials to the ``@login``
endpoint.

.. example-code::

.. code-block:: http-request

POST /@login HTTP/1.1
Accept: application/json
Content-Type: application/json

{
'login': 'admin',
'password': 'admin',
}

.. code-block:: curl

curl -i \
-X POST \
-H "Accept: application/json" \
-H "Content-type: application/json" \
--data-raw '{"login":"admin", "password": "admin"}' \
http://localhost:8080/Plone/@login

.. code-block:: python-requests

requests.post('http://localhost:8080/Plone/@login',
headers={'Accept': 'application/json', 'Content-Type': 'application/json'},
data={'login': 'admin', 'password': 'admin'})


The server responds with a JSON object containing the token.

.. literalinclude:: _json/login.json
:language: js

The token can now be used in subsequent requests by including it in the
``Authorization`` header:

.. code::

GET /Plone HTTP/1.1
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJmdWxsbmFtZSI6IiIsInN1YiI6ImFkbWluIiwiZXhwIjoxNDY0MDQyMTAzfQ.aOyvMwdcIMV6pzC0GYQ3ZMdGaHR1_W7DxT0W0ok4FxI
Accept: application/json

By default the token will expire after 12 hours and thus must be renewed before
expiration. To renew the token simply post to the ``@login-renew`` endpoint.

.. code::

POST /@login-renew HTTP/1.1
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJmdWxsbmFtZSI6IiIsInN1YiI6ImFkbWluIiwiZXhwIjoxNDY0MDQyMTAzfQ.aOyvMwdcIMV6pzC0GYQ3ZMdGaHR1_W7DxT0W0ok4FxI
Accept: application/json

The server returns a JSON object with a new token:

.. literalinclude:: _json/login_renew.json
:language: js


The ``@logout`` endpoint can be used to invalidate tokens. However by default
tokens are not persisted on the server and thus can not be invalidated. To enable
token invaldiation, activate the ``store_tokes`` option in the PAS plugin. If you
need tokens that are valid indefinitely you should also disable the use of Plone's
keyring in the PAS plugin (option ``use_keyring``).

The logout request must contain the existing token in the ``Authorization`` header.

.. code::

POST /@logout HTTP/1.1
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJmdWxsbmFtZSI6IiIsInN1YiI6ImFkbWluIiwiZXhwIjoxNDY0MDQyMTAzfQ.aOyvMwdcIMV6pzC0GYQ3ZMdGaHR1_W7DxT0W0ok4FxI
Accept: application/json

If invalidation succeeds, the server responds with an empty 204 reponse:

.. literalinclude:: _json/logout.json
:language: js
1 change: 1 addition & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
install_requires=[
'setuptools',
'plone.rest >= 1.0a6',
'PyJWT',
],
extras_require={'test': [
'Products.Archetypes',
Expand Down
16 changes: 15 additions & 1 deletion src/plone/restapi/__init__.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
# -*- coding: utf-8 -*-
from AccessControl import allow_module
allow_module('json')
from AccessControl.Permissions import add_user_folders
from Products.PluggableAuthService.PluggableAuthService import (
registerMultiPlugin)
from plone.restapi.pas import plugin

import pkg_resources

allow_module('json')

try:
pkg_resources.get_distribution('plone.app.testing')
except pkg_resources.DistributionNotFound:
Expand All @@ -21,6 +26,15 @@


def initialize(context):
registerMultiPlugin(plugin.JWTAuthenticationPlugin.meta_type)
context.registerClass(
plugin.JWTAuthenticationPlugin,
permission=add_user_folders,
constructors=(plugin.manage_addJWTAuthenticationPlugin,
plugin.addJWTAuthenticationPlugin),
visibility=None,
)

if REGISTER_TEST_TYPES:
from Products.Archetypes.ArchetypeTool import process_types, listTypes
from Products.CMFCore import permissions
Expand Down
8 changes: 8 additions & 0 deletions src/plone/restapi/configure.zcml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,14 @@
provides="Products.GenericSetup.interfaces.EXTENSION"
/>

<!-- Register "various" import step -->
<genericsetup:importStep
name="plone.restapi"
title="plone.restapi special import handlers"
description=""
handler="plone.restapi.setuphandlers.import_various"
/>

<include package="plone.rest" file="configure.zcml" />
<include package=".services" />
<include package=".serializer" />
Expand Down
Empty file.
33 changes: 33 additions & 0 deletions src/plone/restapi/pas/add_plugin.zpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:tal="http://xml.zope.org/namespaces/tal">
<body>
<h1 tal:replace="structure here/manage_page_header">Header</h1>

<h2 tal:define="form_title string:JWT Authentication Plugin"
tal:replace="structure here/manage_form_title">Form Title</h2>

<p class="form-help">
Plone PAS plugin for authentication with JSON web tokens (JWT)
</p>

<form action="addJWTAuthenticationPlugin" method="post">
<table>
<tr>
<td class="form-label">Id</td>
<td><input type="text" name="id_" tal:attributes="value request/id|string:jwt_auth"/></td>
</tr>
<tr>
<td class="form-label">Title</td>
<td><input type="text" name="title"/></td>
</tr>
<tr>
<td colspan="2">
<div class="form-element">
<input type="submit" value="Add"/>
</div>
</td>
</tr>
</table>
</form>
</body>
</html>
47 changes: 47 additions & 0 deletions src/plone/restapi/pas/config.zpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
<h1 tal:replace="structure here/manage_page_header"> PAGE HEADER </h1>
<h2 tal:replace="structure here/manage_tabs"> PAGE HEADER </h2>

<h3>JWT Authentication</h3>

<p class="form-help">
Plone PAS plugin for authentication with JSON web tokens (JWT).
</p>

<form action="manage_updateConfig" method="post">
<table>
<tr valign="top">
<td><div class="form-label">Token Validity Timeout (in seconds)</div>
<div class="form-help">After this, the token is invalid and the user
must login again. Set to 0 for the token to remain valid indefinitely.</div>
</td>
<td><input type="text" name="token_timeout"
tal:attributes="value context/token_timeout|nothing"/></td>
</tr>
<tr>
<td align="left" valign="top">
<input type="checkbox" name="use_keyring" id="use-keyring"
tal:attributes="checked python: context.use_keyring and 'checked'"/>&nbsp;<label class="form-label" for="use-keyring">Use Keyring</label>
<div class="form-help">If enabled, tokens are signed with a secret from
Plone's keyring. If you want tokens that remain valid indefinitely you should disable this.</div>
</td>
</tr>
<tr>
<td align="left" valign="top">
<input type="checkbox" name="store_tokens" id="store-tokens"
tal:attributes="checked python: context.store_tokens and 'checked'"/>&nbsp;<label class="form-label" for="store-tokens">Store tokens</label>
<div class="form-help">By default tokens are not stored on the server and
thus can't be invalidated. If enabled, tokens that don't expire can be invalidated.</div>
</td>
</tr>
<tr>
<td colspan="2">
<div class="form-element">
<input type="submit" value="Update"/>
</div>
</td>
</tr>
</table>
</form>


<h1 tal:replace="structure here/manage_page_footer"> PAGE FOOTER </h1>
Loading