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

Add support for builtin API Gateway authorizers #381

Closed
wants to merge 11 commits into from
86 changes: 86 additions & 0 deletions docs/source/api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,32 @@ Chalice
you would like more control over how CORS is configured, you can
provide an instance of :class:`CORSConfig`.

.. method:: authorizer(name, \*\*options)

Register a built-in authorizer.

.. code-block:: python

from chalice import Chalice, AuthResponse

app = Chalice(app_name="appname")

@app.authorizer(ttl_seconds=30)
def my_auth(auth_request):
# Validate auth_request.token, and then:
return AuthResponse(routes=['/'], principal_id='username')

@app.route('/', authorizer=my_auth)
def viewfunction(value):
pass

:param ttl_seconds: The number of seconds to cache this response.
Subsequent requests that require this authorizer will use a
cached response if available. The default is 300 seconds.

:param execution_role: An optional IAM role to specify when invoking
the Lambda function associated with the built-in authorizer.


Request
=======
Expand Down Expand Up @@ -257,6 +283,66 @@ for an ``@app.route(authorizer=...)`` call:
The header where the auth token will be specified.


Built-in Authorizers
--------------------

These classes are used when defining built-in authoriers in Chalice.

.. class:: AuthRequest(auth_type, token, method_arn)

An instance of this class is passed as the first argument
to an authorizer defined via ``@app.authorizer()``. You
generally do not instantiate this class directly.

.. attribute:: auth_type

The type of authentication

.. attribute:: token

The authorization token. This is usually the value of the
``Authorization`` header.

.. attribute:: method_arn

The ARN of the API gateway being authorized.

.. class:: AuthResponse(routes, principal_id, context=None)

.. attribute:: routes

A list of authorized routes. Each element in the list
can either by a string route such as `"/foo/bar"` or
an instance of ``AuthRoute``. If you specify the URL as
a string, then all supported HTTP methods will be authorized.
If you want to specify which HTTP methods are allowed, you
can use ``AuthRoute``.

.. attribute:: principal_id

The principal id of the user.

.. attribute:: context

An optional dictionary of key value pairs. This dictionary
will be accessible in the ``app.current_request.context``
in all subsequent authorized requests for this user.

.. class:: AuthRoute(path, methods)

This class be used in the ``routes`` attribute of a
:class:`AuthResponse` instance to get fine grained control
over which HTTP methods are allowed for a given route.

.. attribute:: path

The allowed route specified as a string

.. attribute:: methods

A list of allowed HTTP methods.


APIGateway
==========

Expand Down
1 change: 1 addition & 0 deletions docs/source/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ Topics
topics/packaging
topics/pyversion
topics/cfn
topics/authorizers


API Reference
Expand Down
219 changes: 219 additions & 0 deletions docs/source/topics/authorizers.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,219 @@
Authorization
=============

Chalice supports multiple mechanisms for authorization. This topic
covers how you can integrate authorization into your Chalice applications.

In Chalice, all the authorizers are configured per-route and specified
using the ``authorizer`` kwarg to an ``@app.route()`` call. You
control which type of authorizer to use based on what's passed as the
``authorizer`` kwarg. You can use the same authorizer instance for
multiple routes.

The first set of authorizers chalice supports cover the scenario where
you have some existing authorization mechanism that you just want your
Chalice app to use.

Chalice also supports built-in authorizers, which allows Chalice to
manage your custom authorizers as part of ``chalice deploy``. This is
covered in the Built-in Authorizers section.


AWS IAM Authorizer
------------------

The IAM Authorizer allows you to control access to API Gateway with
`IAM permissions`_

To associate an IAM authorizer with a route in chalice, you use the
:class:`IAMAUthorizer` class:

.. code-block:: python

authorizer = IAMAuthorizer()

@app.route('/iam-auth', methods=['GET'], authorizer=authorizer)
def authenticated():
return {"success": True}


See the `API Gateway documentation
<http://docs.aws.amazon.com/apigateway/latest/developerguide/permissions.html>`__
for more information on controlling access to API Gateway with IAM permissions.

Amazon Cognito User Pools
-------------------------

In addition to using IAM roles and policies with the :class:`IAMAuthorizer` you
can also use a `Cognito user pools`_ to control who can access your Chalice
app. A cognito user pool serves as your own identity provider to maintain a
user directory.

To integrate Cognito user pools with Chalice, you'll need to have an existing
cognito user pool configured.


.. code-block:: python

authorizer = CognitoUserPoolAuthorizer(
'MyPool', provider_arns=['arn:aws:cognito:...:userpool/name'])

@app.route('/user-pools', methods=['GET'], authorizer=authorizer)
def authenticated():
return {"sucecss": True}


Within a request, you can access the ``app.current_request.context`` object
for metadata about the authenticated request.

For more information about using Cognito user pools with API Gateway,
see the `Use Amazon Cognito User Pools documentation
<http://docs.aws.amazon.com/apigateway/latest/developerguide/apigateway-integrate-with-cognito.html>`__.


Custom Authorizers
------------------

API Gateway also lets you write custom authorizers using a Lambda function.
You can configure a Chalice route to use a pre-existing Lambda function as
a custom authorizer. If you also want to write and manage your Lambda
authorizer using Chalice, see the next section, Built-in Authorizers.

To connect an existing Lambda function as a custom authorizer in chalice,
you use the ``CustomAuthorizer`` class:

.. code-block:: python

authorizer = CustomAuthorizer(
'MyCustomAuth', header='Authorization',
authorizer_uri=('arn:aws:apigateway:region:lambda:path/2015-03-01'
'/functions/arn:aws:lambda:region:account-id:'
'function:FunctionName/invocations'))

@app.route('/custom-auth', methods=['GET'], authorizer=authorizer)
def authenticated():
return {"success": True}


Built-in Authorizers
--------------------

The ``IAMAuthorizer``, ``CognitoUserPoolAuthorizer``, and the
``CustomAuthorizer`` classes are all for cases where you have existing
resources for managing authorization and you want to wire them together with
your Chalice app. A Built-in authorizer is used when you'd like to write your
custom authorizer in Chalice, and have the additional Lambda functions managed
when you run ``chalice deploy/delete``. This section will cover how to use the
built-in authorizers in chalice.

Creating an authorizer in chalice requires you use the ``@app.authorizer``
decorator to a function. The function must accept a single arg, which will be
an instance of :class:`AuthRequest`. The function must return a
:class:`AuthResponse`. As an example, we'll port the example from the `API
Gateway documentation`_. First, we'll show the code and then walk through it:

.. code-block:: python

from chalice import Chalice, AuthResponse

app = Chalice(app_name='demoauth1')


@app.authorizer()
def demo_auth(auth_request):
token = auth_request.token
# This is just for demo purposes as shown in the API Gateway docs.
# Normally you'd call an oauth provider, validae the
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

validae -> validate

# jwt token, etc.
# In this exampe, the token is treated as the status for demo
# purposes.
if token == 'allow':
return AuthResponse(routes=['/'], principal_id='user')
else:
return AuthResponse(routes=[], principal_id='user')


@app.route('/', authorizer=demo_auth)
def index():
return {'context': app.current_request.context}


In the example above we define a built-in authorizer by decorating
the ``demo_auth`` function with the ``@app.authorizer()`` decorator.
Note you must use ``@app.authorizer()`` and not ``@app.authorizer``.
A built-in authorizer function has this type signature::

def auth_handler(auth_request: AuthRequest) -> AuthResponse: ...

Within the auth handler you must determine if the request is
authorized or not. The ``AuthResponse`` contains the allowed
URLs as well as the principal id of the user. You can optionally
return a dictionary of key value pairs (as the ``context`` kwarg).
This dictionary will be passed through on subsequent requests.
In our example above we're not using the context dictionary.

Now let's deploy our app. As usual, we just need to run
``chalice deploy`` and chalice will automatically deploy all the
necessary Lambda functions for us.

Now when we try to make a request, we'll get an Unauthorized error::

$ http https://api.us-west-2.amazonaws.com/dev/
HTTP/1.1 401 Unauthorized

{
"message": "Unauthorized"
}

If we add the appropriate authorization header, we'll see the call succeed::

$ http https://api.us-west-2.amazonaws.com/dev/ 'Authorization: allow'
HTTP/1.1 200 OK

{
"context": {
"accountId": "12345",
"apiId": "api",
"authorizer": {
"principalId": "user"
},
"httpMethod": "GET",
"identity": {
"accessKey": null,
"accountId": null,
"apiKey": "",
"caller": null,
"cognitoAuthenticationProvider": null,
"cognitoAuthenticationType": null,
"cognitoIdentityId": null,
"cognitoIdentityPoolId": null,
"sourceIp": "1.1.1.1",
"user": null,
"userAgent": "HTTPie/0.9.9",
"userArn": null
},
"path": "/dev/",
"requestId": "d35d2063-56be-11e7-9ce1-dd61c24a3668",
"resourceId": "id",
"resourcePath": "/",
"stage": "dev"
}
}

The low level API for API Gateway's custom authorizer feature requires
that an IAM policy must be returned. The :class:`AuthResponse` class we're
using is a wrapper over building the IAM policy ourself. If you want
low level control and would prefer to contruct the IAM policy yourself
you can return a dictionary of the IAM policy instead of an instance of
:class:`AuthResponse`. If you do that, the dictionary is returned
without modification back to API Gateway.

For more information on custom authorizers, see the
`Use API Gateway Custom Authorizers
<http://docs.aws.amazon.com/apigateway/latest/developerguide/use-custom-authorizer.html>`__
page in the API Gateway user guide.


.. _IAM permissions: http://docs.aws.amazon.com/IAM/latest/UserGuide/access_permissions.html
.. _Cognito User Pools: http://docs.aws.amazon.com/cognito/latest/developerguide/cognito-user-identity-pools.html
.. _API Gateway documentation: http://docs.aws.amazon.com/apigateway/latest/developerguide/use-custom-authorizer.html#api-gateway-custom-authorizer-lambda-function-create