diff options
author | Ib Lundgren <ib.lundgren@gmail.com> | 2013-06-18 21:33:25 +0100 |
---|---|---|
committer | Ib Lundgren <ib.lundgren@gmail.com> | 2013-06-18 21:33:25 +0100 |
commit | ded77d72addaa46d718d84643616c8bba5fab43d (patch) | |
tree | ae78b3b2a2671b1149f93d82d90b25af9ab8c6de /docs | |
parent | 8ba2b3a6c7b5ba94eae93187a0f4ac6dbe80d22f (diff) | |
download | oauthlib-ded77d72addaa46d718d84643616c8bba5fab43d.tar.gz |
Updated documentation for OAuth 1 provider. #95
Diffstat (limited to 'docs')
-rw-r--r-- | docs/oauth1/endpoints.rst | 20 | ||||
-rw-r--r-- | docs/oauth1/preconfigured_servers.rst | 18 | ||||
-rw-r--r-- | docs/oauth1/security.rst | 44 | ||||
-rw-r--r-- | docs/oauth1/server.rst | 528 | ||||
-rw-r--r-- | docs/oauth1/validator.rst | 6 |
5 files changed, 512 insertions, 104 deletions
diff --git a/docs/oauth1/endpoints.rst b/docs/oauth1/endpoints.rst new file mode 100644 index 0000000..b01d7f8 --- /dev/null +++ b/docs/oauth1/endpoints.rst @@ -0,0 +1,20 @@ +Provider endpoints +================== + +Each endpoint is responsible for one step in the OAuth 1 workflow. They can be +used either independently or in a combination. They depend on the use of a +:doc:`validator`. + +See :doc:`preconfigured_servers` for available composite endpoints/servers. + +.. autoclass:: oauthlib.oauth1.AuthorizationEndpoint + :members: + +.. autoclass:: oauthlib.oauth1.AccessTokenEndpoint + :members: + +.. autoclass:: oauthlib.oauth1.RequestTokenEndpoint + :members: + +.. autoclass:: oauthlib.oauth1.ResourceEndpoint + :members: diff --git a/docs/oauth1/preconfigured_servers.rst b/docs/oauth1/preconfigured_servers.rst new file mode 100644 index 0000000..7f7f386 --- /dev/null +++ b/docs/oauth1/preconfigured_servers.rst @@ -0,0 +1,18 @@ +Preconfigured all-in-one servers +================================ + +A pre configured server is an all-in-one endpoint serving a specific class of +application clients. As the individual endpoints, they depend on the use of a +:doc:`validator`. + +Construction is simple, only import your validator and you are good to go:: + + from your_validator import your_validator + from oauthlib.oauth1 import WebApplicationServer + + server = WebApplicationServer(your_validator) + +All endpoints are documented in :doc:`endpoints`. + +.. autoclass:: oauthlib.oauth1.WebApplicationServer + :members: diff --git a/docs/oauth1/security.rst b/docs/oauth1/security.rst new file mode 100644 index 0000000..fa2180e --- /dev/null +++ b/docs/oauth1/security.rst @@ -0,0 +1,44 @@ +A few important facts regarding OAuth security +============================================== + + * **OAuth without SSL is a Bad Idea™** and it's strongly recommended to use + SSL for all interactions both with your API as well as for setting up + tokens. An example of when it's especially bad is when sending POST + requests with form data, this data is not accounted for in the OAuth + signature and a successfull man-in-the-middle attacker could swap your + form data (or files) to whatever he pleases without invalidating the + signature. This is an even bigger issue if you fail to check + nonce/timestamp pairs for each request, allowing an attacker who + intercept your request to replay it later, overriding your initial + request. **Server defaults to fail all requests which are not made over + HTTPS**, you can explicitely disable this using the enforce_ssl + property. + + * **Tokens must be random**, OAuthLib provides a method for generating + secure tokens and it's packed into ``oauthlib.common.generate_token``, + use it. If you decide to roll your own, use ``random.SystemRandom`` + which is based on ``os.urandom`` rather than the default ``random`` + based on the effecient but not truly random Mersenne Twister. + Predicatble tokens allow attackers to bypass virtually all defences + OAuth provides. + + * **Timing attacks are real** and more than possible if you host your + application inside a shared datacenter. Ensure all ``validate_`` methods + execute in near constant time no matter which input is given. This will + be covered in more detail later. Failing to account for timing attacks + could **enable attackers to enumerate tokens and successfully guess HMAC + secrets**. Note that RSA keys are protected through RSA blinding and are + not at risk. + + * **Nonce and timestamps must be checked**, do not ignore this as it's a + simple and effective way to prevent replay attacks. Failing this allows + online bruteforcing of secrets which is not something you want. + + * **Whitelisting is your friend** and effectively eliminates SQL injection + and other nasty attacks on your precious data. More details on this in + the ``check_`` methods. + + * **Require all callback URIs to be registered before use**. OAuth providers + are in the unique position of being able to restrict which URIs may be + submitted, making validation simple and safe. This registration should + be done in your Application management interface. diff --git a/docs/oauth1/server.rst b/docs/oauth1/server.rst index 5beb3b7..d1616c4 100644 --- a/docs/oauth1/server.rst +++ b/docs/oauth1/server.rst @@ -1,122 +1,442 @@ OAuth 1: Creating a Provider ============================ -Note that the current OAuth1 provider interface will change into one resembling -the work in progress OAuth 2 provider in a not too distant future. More -information in `issue #95`_. - -.. _`issue #95`: https://github.com/idan/oauthlib/issues/95 - -Implementing an OAuth provider is simple with OAuthLib. It is done by inheriting -from ``oauthlib.oauth1.rfc5849.Server`` and overloading a few key methods. The -base class provide a secure by default implementation including a -``verify_request`` method as well as several input validation methods, all -configurable using properties. While it is straightforward to use OAuthLib -directly with your web framework of choice it is worth first exploring whether -there is an OAuthLib based OAuth provider plugin available for your framework. - -**A few important facts regarding OAuth security** - - * **OAuth without SSL is a Bad Idea™** and it's strongly recommended to use - SSL for all interactions both with your API as well as for setting up - tokens. An example of when it's especially bad is when sending POST - requests with form data, this data is not accounted for in the OAuth - signature and a successfull man-in-the-middle attacker could swap your - form data (or files) to whatever he pleases without invalidating the - signature. This is an even bigger issue if you fail to check - nonce/timestamp pairs for each request, allowing an attacker who - intercept your request to replay it later, overriding your initial - request. **Server defaults to fail all requests which are not made over - HTTPS**, you can explicitely disable this using the enforce_ssl - property. - - * **Tokens must be random**, OAuthLib provides a method for generating - secure tokens and it's packed into ``oauthlib.common.generate_token``, - use it. If you decide to roll your own, use ``random.SystemRandom`` - which is based on ``os.urandom`` rather than the default ``random`` - based on the effecient but not truly random Mersenne Twister. - Predicatble tokens allow attackers to bypass virtually all defences - OAuth provides. - - * **Timing attacks are real** and more than possible if you host your - application inside a shared datacenter. Ensure all ``validate_`` methods - execute in near constant time no matter which input is given. This will - be covered in more detail later. Failing to account for timing attacks - could **enable attackers to enumerate tokens and successfully guess HMAC - secrets**. Note that RSA keys are protected through RSA blinding and are - not at risk. - - * **Nonce and timestamps must be checked**, do not ignore this as it's a - simple and effective way to prevent replay attacks. Failing this allows - online bruteforcing of secrets which is not something you want. - - * **Whitelisting is your friend** and effectively eliminates SQL injection - and other nasty attacks on your precious data. More details on this in - the ``check_`` methods. - - * **Require all callback URIs to be registered before use**. OAuth providers - are in the unique position of being able to restrict which URIs may be - submitted, making validation simple and safe. This registration should - be done in your Application management interface. - -**Verifying requests** - - Request verification is provided through the ``Server.verify_request`` - method which has the following signature:: - - verify_request(self, uri, http_method=u'GET', body=None, headers=None, - require_resource_owner=True, - require_verifier=False, - require_realm=False, - required_realm=None) - - There are three types of verifications you will want to perform, all which - could be altered through the use of a realm parameter if you choose to - allow/require this. Note that if verify_request returns false a HTTP - 401Unauthorized should be returned. If a ValueError is raised a HTTP 400 Bad - Request response should be returned. All request verifications will look - similar to the following:: +OAuthLib is a dependency free library that may be used with any web +framework. That said, there are framework specific helper libraries +to make your life easier. +- For Django there is `django-oauth-toolkit`_. +- For Flask there is `flask-oauthlib`_. + +If there is no support for your favourite framework and you are interested +in providing it then you have come to the right place. OAuthLib can handle +the OAuth logic and leave you to support a few framework and setup specific +tasks such as marshalling request objects into URI, headers and body arguments +as well as provide an interface for a backend to store tokens, clients, etc. + +.. _`django-oauth-toolkit`: https://github.com/evonove/django-oauth-toolkit +.. _`flask-oauthlib`: https://github.com/lepture/flask-oauthlib + +.. contents:: Tutorial Contents + :depth: 3 + + +1. Create your datastore models +------------------------------- + +These models will represent various OAuth specific concepts. There are a few +important links between them that the security of OAuth is based on. Below +is a suggestion for models and why you need certain properties. There is +also example SQLAlchemy model fields which should be straightforward to +translate to other ORMs such as Django and the Appengine Datastore. + +1.1 User (or Resource Owner) +^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The user of your site which resources might be access by clients upon +authorization from the user. Below is a crude example of a User model, yours +is likely to differ and the structure is not important. Neither is how the user +authenticates, as long as it does before authorizing:: + + Base = sqlalchemy.ext.declarative.declarative_base() + class ResourceOwner(Base): + __tablename__ = "users" + + id = sqlalchemy.Column(sqlalchemy.Integer, primary_key=True) + name = sqlalchemy.Column(sqlalchemy.String) + email = sqlalchemy.Column(sqlalchemy.String) + password = sqlalchemy.Column(sqlalchemy.String) + +1.2 Client (or Consumer) +^^^^^^^^^^^^^^^^^^^^^^^^ + +The client interested in accessing protected resources. + +**Client Identifier / Consumer key**: + Required. The identifier the client will use during the OAuth + workflow. Structure is up to you and may be a simple UID:: + + client_key = sqlalchemy.Column(sqlalchemy.String) + +**Client secret**: + Required for HMAC-SHA1 and PLAINTEXT. The secret the client will use when + verifying requests during the OAuth workflow. Has to be accesible as + plaintext (i.e. not hashed) since it is used to recreate and validate + request signatured:: + + client_secret = sqlalchemy.Column(sqlalchemy.String) + +**Cient public key**: + Required for RSA-SHA1. The public key used to verify the signature of + requests signed by the clients private key:: + + rsa_key = sqlalchemy.Column(sqlalchemy.String) + +**User**: + Recommended. It is common practice to link each client with one of + your existing users. Whether you do associate clients and users or + not, ensure you are able to protect yourself against malicious + clients:: + + user = Column(Integer, ForeignKey("users.id")) + +**Realms**: + Required. The list of realms the client may request access to. While realm + use is largely undocumented in the spec you may think of them as very + similar to OAuth 2 scopes.:: + + # You could represent it either as a list of keys or by serializing + # the scopes into a string. + realms = sqlalchemy.Column(sqlalchemy.String) + + # You might also want to mark a certain set of scopes as default + # scopes in case the client does not specify any in the authorization + default_realms = sqlalchemy.Column(sqlalchemy.String) + +**Redirect URIs**: + These are the absolute URIs that a client may use to redirect to after + authorization. You should never allow a client to redirect to a URI + that has not previously been registered:: + + # You could represent the URIs either as a list of keys or by + # serializing them into a string. + redirect_uris = sqlalchemy.Column(sqlalchemy.String) + + # You might also want to mark a certain URI as default in case the + # client does not specify any in the authorization + default_redirect_uri = sqlalchemy.Column(sqlalchemy.String) + +1.3 Request Token + Verifier +^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +In OAuth 1 workflow the first step is obtaining/providing a request token. This +token captures information about the client, its callback uri and realms +requested. This step is not present in OAuth2 as these credentials are supplied +directly in the authorization step. + +When the request token is first created the user is unknown. The user is +associated with a request token during the authorization step. After successful +authorization the client is presented with a verifier code (should be linked to +request token) as a proof of authorization. This verifier code is later used to +obtain an access token. + +**Client**: + Association with the client to whom the request token was given:: + + client = Column(Integer, ForeignKey("clients.id")) + +**User**: + Association with the user to which protected resources this token + requests access:: + + user = Column(Integer, ForeignKey("users.id")) + +**Realms**: + Realms to which the token is bound. Attempt to access protected + resources outside these realms will be denied:: + + # You could represent it either as a list of keys or by serializing + # the scopes into a string. + realms = sqlalchemy.Column(sqlalchemy.String) + +**Redirect URI**: + The callback URI used to redirect back to the client after user + authorization is completed:: + + redirect_uri = sqlalchemy.Column(sqlalchemy.String) + +**Request Token**: + An unguessable unique string of characters:: + + request_token = sqlalchemy.Column(sqlalchemy.String) + +**Request Token Secret**: + An unguessable unique string of characters. This is a temporary secret used + by the HMAC-SHA1 and PLAINTEXT signature methods when obtaining an + access token later:: + + request_token_secret = sqlalchemy.Column(sqlalchemy.String) + +**Authorization Verifier**: + An unguessable unique string of characters. This code asserts that the user + has given the client authorization to access the requested realms. It is + initial nil when the client obtains the request token in the first step, and + set after user authorization is given in the second step:: + + verifier = sqlalchemy.Column(sqlalchemy.String) + +1.4 Access Token +^^^^^^^^^^^^^^^^ + +Access tokens are provided to clients able to present a valid request token +together with its associated verifier. It will allow the client to access +protected resources and is normally not associated with an expiration. Although +you should consider expiring them as it increases security dramatically. + +The user and realms will need to be transferred from the request token to the +access token. It is possible that the list of authorized realms is smaller +than the list of requested realms. Clients can observe whether this is the case +by comparing the `oauth_realms` parameter given in the token reponse. This way +of indicated change of realms is backported from OAuth2 scope behaviour and is +not in the OAuth 1 spec. + +**Client**: + Association with the client to whom the access token was given:: + + client = Column(Integer, ForeignKey("clients.id")) + +**User**: + Association with the user to which protected resources this token + grants access:: + + user = Column(Integer, ForeignKey("users.id")) + +**Realms**: + Realms to which the token is bound. Attempt to access protected + resources outside these realms will be denied:: + + # You could represent it either as a list of keys or by serializing + # the scopes into a string. + realms = sqlalchemy.Column(sqlalchemy.String) + +**Access Token**: + An unguessable unique string of characters:: + + access_token = sqlalchemy.Column(sqlalchemy.String) + +**Access Token Secret**: + An unguessable unique string of characters. This secret is used + by the HMAC-SHA1 and PLAINTEXT signature methods when accessing protected + resources:: + + access_token_secret = sqlalchemy.Column(sqlalchemy.String) + +2. Implement a validator +------------------------ + +The majority of the work involved in implementing an OAuth 1 provider +relates to mapping various validation and persistence methods to a storage +backend. The not very accurately named interface you will need to implement +is called a :doc:`RequestValidator <validator>` (name suggestions welcome). + +An example of a very basic implementation of the ``validate_client_key`` method +can be seen below:: + + from oauthlib.oauth1 import RequestValidator + + # From the previous section on models + from my_models import Client + + class MyRequestValidator(RequestValidator): + + def validate_client_key(self, client_key, request): + try: + Client.query.filter_by(client_key=client_key).one() + return True + except NoResultFound: + return False + +The full API you will need to implement is available in the +:doc:`RequestValidator <validator>` section. You might not need to implement +all methods depending on which signature methods you wish to support. + +Relevant sections include: + +.. toctree:: + :maxdepth: 1 + + validator + security + + +3. Create your composite endpoint +--------------------------------- + +Each of the endpoints can function independently from each other, however +for this example it is easier to consider them as one unit. An example of a +pre-configured all-in-one OAuth 1 RFC compliant [#compliant]_ endpoint is +given below:: + + # From the previous section on validators + from my_validator import MyRequestValidator + + from oauthlib.oauth1 import WebApplicationServer + + validator = MyRequestValidator() + server = WebApplicationServer(validator) + + +Relevant sections include: + +.. toctree:: + :maxdepth: 1 + + preconfigured_servers + +.. [#compliant] Standard 3-legged OAuth 1 as defined in the RFC specification. + + +4. Create your endpoint views +----------------------------- + +Standard 3 legged OAuth requires 4 views, request and access token together with +pre- and post-authorization. In addition an error view should be defined +where users can be informed of invalid/malicious authorization requests. + +The example uses Flask but should be transferable to any framework. + +.. code-block:: python + + from flask import Flask, redirect, Response, request, url_for + from oauthlib.oauth1 import OAuth1Error + + + app = Flask(__name__) + + + @app.route('/request_token', methods=['POST']) + def request_token(): + _, h, b, s = provider.create_request_token_response(request.url, + http_method=request.method, + body=request.data, + headers=request.headers) + return Response(b, status=s, headers=h) + + + @app.route('/authorize', methods=['GET']) + def pre_authorize(): + realms, credentials = provider.get_realms_and_credentials(request.url, + http_method=request.method, + body=request.data, + headers=request.headers) + client_key = credentials.get('resource_owner_key', 'unknown') + response = '<h1> Authorize access to %s </h1>' % client_key + response += '<form method="POST" action="/authorize">' + for realm in realms or []: + response += ('<input type="checkbox" name="realms" ' + + 'value="%s"/> %s' % (realm, realm)) + response += '<input type="submit" value="Authorize"/>' + return response + + + @app.route('/authorize', methods=['POST']) + def post_authorize(): + realms = request.form.getlist('realms') try: - authorized = server.verify_request(uri, http_method, body, headers) - if not authorized: - # return a HTTP 401 Unauthorized response - pass - else: - # Create, save and return request token/access token/protected resource - # or whatever you had in mind that required OAuth - pass - except ValueError: - # return a HTTP 400 Bad Request response - pass + u, _, _, _ = provider.create_authorization_response(request.url, + http_method=request.method, + body=request.data, + headers=request.headers, + realms=realms) + return redirect(u) + except OAuth1Error as e: + return redirect(e.in_uri(url_for('/error'))) + + + @app.route('/access_token', methods=['POST']) + def access_token(): + _, h, b, s = provider.create_access_token_response(request.url, + http_method=request.method, + body=request.data, + headers=request.headers) + return Response(b, status=s, headers=h) + + + @app.route('/error', methods=['GET']) + def error(): + # Invalid request token will be most likely + # Could also be an attempt to change the authorization form to try and + # authorize realms outside the allowed for this client. + return 'client did something bad' + +5. Protect your APIs using realms +--------------------------------- + +Let's define a decorator we can use to protect the views. + +.. code-block:: python + + + def oauth_protected(realms=None): + def wrapper(f): + @functools.wraps(f) + def verify_oauth(*args, **kwargs): + v, r = provider.validate_protected_resource_request(request.url, + http_method=request.method, + body=request.data, + headers=request.headers, + valid_realms=realms or []) + if v: + return f(*args, **kwargs) + else: + return abort(403) + return verify_oauth + return wrapper + +At this point you are ready to protect your API views with OAuth. Take some +time to come up with a good set of realms as they can be very powerful in +controlling access. + +.. code-block:: python + + @app.route('/secret', methods=['GET']) + @oauth_protected(realms=['secret']) + def protected_resource(): + return 'highly confidential' + +6. Try your provider with a quick CLI client +-------------------------------------------- + +This example assumes you use the client key `key` and client secret `secret` +shown below as well as run your flask server locally on port `5000`. + +.. code-block:: bash + + $ pip install requests requests-oauthlib - The only change will be parameters to the verify_request method. +.. code-block:: python - #. Requests to obtain request tokens, these may include an optional - redirection URI parameter:: + >>> key = 'abcdefghijklmnopqrstuvxyzabcde' + >>> secret = 'foo' - authorized = server.verify_request(uri, http_method, body, headers, - require_resource_owner=False) + >>> # OAuth endpoints given in the Bitbucket API documentation + >>> request_token_url = 'http://127.0.0.1:5000/request_token' + >>> authorization_base_url = 'http://127.0.0.1:5000/authorize' + >>> access_token_url = 'http://127.0.0.1:5000/access_token' - #. Requests to obtain access tokens, these should always include a verifier - and a resource owner key:: + >>> # 2. Fetch a request token + >>> from requests_oauthlib import OAuth1Session + >>> oauth = OAuth1Session(key, client_secret=secret, + >>> callback_uri='http://127.0.0.1/cb') + >>> oauth.fetch_request_token(request_token_url) - authorized = server.verify_request(uri, http_method, body, headers, - require_verifier=True) + >>> # 3. Redirect user to your provider implementation for authorization + >>> authorization_url = oauth.authorization_url(authorization_base_url) + >>> print 'Please go here and authorize,', authorization_url - #. Requests to protected resources:: + >>> # 4. Get the authorization verifier code from the callback url + >>> redirect_response = raw_input('Paste the full redirect URL here:') + >>> oauth.parse_authorization_response(redirect_response) - authorized = server.verify_request(uri, http_method, body, headers) + >>> # 5. Fetch the access token + >>> oauth.fetch_access_token(access_token_url) + >>> # 6. Fetch a protected resource, i.e. user profile + >>> r = oauth.get('http://127.0.0.1:5000/secret') + >>> print r.content -**Configuring check methods and their respective properties** +7. Let us know how it went! +--------------------------- - There are a number of input validation checks that perform white listing of - input parameters. I hope to document them soon but for now please refer to - the Server source code found in oauthlib.oauth1.rfc5849.__init__.py. +Drop a line in our `G+ community`_ or open a `GitHub issue`_ =) -**Methods that must be overloaded** +.. _`G+ community`: https://plus.google.com/communities/101889017375384052571 +.. _`GitHub issue`: https://github.com/idan/oauthlib/issues/new -.. autoclass:: oauthlib.oauth1.rfc5849.Server - :members: +If you run into issues it can be helpful to enable debug logging:: + import logging + import sys + log = logging.getLogger('oauthlib') + log.addHandler(logging.StreamHandler(sys.stdout)) + log.setLevel(logging.DEBUG) diff --git a/docs/oauth1/validator.rst b/docs/oauth1/validator.rst new file mode 100644 index 0000000..398e239 --- /dev/null +++ b/docs/oauth1/validator.rst @@ -0,0 +1,6 @@ +================= +Request Validator +================= + +.. autoclass:: oauthlib.oauth1.RequestValidator + :members: |