.. _specs/rfc8628:

RFC8628: OAuth 2.0 Device Authorization Grant
=============================================

.. meta::
    :description: Python API references on RFC8628 DeviceAuthorizationEndpoint
        and DeviceCodeGrant with Authlib implementation.

.. module:: authlib.oauth2.rfc8628

This section contains the generic implementation of RFC8628_. OAuth 2.0 Device
Authorization Grant is usually used when devices have limited input capabilities
or lack a suitable browser, such as smart TVs, media consoles, picture frames,
printers and etc.

To integrate with Authlib :ref:`flask_oauth2_server` or :ref:`django_oauth2_server`,
developers MUST implement the missing methods of the two classes:

1. :class:`DeviceAuthorizationEndpoint`
2. :class:`DeviceCodeGrant`

Device Authorization Endpoint
-----------------------------

There are two missing methods that developers MUST implement::

    from authlib.oauth2.rfc8628 import DeviceAuthorizationEndpoint

    class MyDeviceAuthorizationEndpoint(DeviceAuthorizationEndpoint):
        def get_verification_uri(self):
            return 'https://example.com/active'

        def save_device_credential(self, client_id, scope, data):
            credential = DeviceCredential(
                client_id=client_id,
                scope=scope,
                **data
            )
            credential.save()

    # register it to authorization server
    authorization_server.register_endpoint(MyDeviceAuthorizationEndpoint)

``get_verification_uri`` is the URL that end user will use their browser to
log in and authenticate. See below "Verification Endpoint".

After the registration, you can create a response with::

    @app.route('/device_authorization', methods=['POST'])
    def device_authorization():
        return server.create_endpoint_response('device_authorization')

Device Code Grant
-----------------

With Authlib ``.register_grant``, we can add ``DeviceCodeGrant`` easily.
But first, we need to implement the missing methods::

    from authlib.oauth2.rfc8628 import DeviceCodeGrant

    class MyDeviceCodeGrant(DeviceCodeGrant):
        def query_device_credential(self, device_code):
            return DeviceCredential.query(device_code=device_code)

        def query_user_grant(self, user_code):
            data = redis.get('oauth_user_grant:' + user_code)
            if not data:
                return None

            user_id, allowed = data.split()
            user = User.query.get(user_id)
            return user, bool(allowed)

        def should_slow_down(self, credential, now):
            # developers can return True/False based on credential and now
            return False

    authorization_server.register_grant(MyDeviceCodeGrant)

Note ``query_user_grant``, we are fetching data from redis. This data
was saved from verification endpoint when end user granted the request.

Verification Endpoint
---------------------

Developers MUST implement this part by themselves. Here is a hint on
how to implement this endpoint::

    @app.route('/active', methods=['GET', 'POST'])
    @login_required
    def verify_device_code():
        if request.method == 'GET':
            return render_template('verification.html')

        allowed = request.form['allowed']
        user_code = request.form['user_code']
        key = 'oauth_user_grant:' + user_code
        redis.set(key, f'{current_user.id} {allowed}', 12)
        return render_template('verification.html')

Check points:

1. route should match ``get_verification_uri`` in Device Authorization Endpoint
2. user grant should match ``query_user_grant`` in Device Code Grant


.. _RFC8628: https://tools.ietf.org/html/rfc8628


API Reference
-------------

.. autoclass:: DeviceAuthorizationEndpoint
    :member-order: bysource
    :members:

.. autoclass:: DeviceCodeGrant
    :member-order: bysource
    :members:
    :inherited-members:
