File: endpoint.py

package info (click to toggle)
python-authlib 1.6.1-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 3,016 kB
  • sloc: python: 26,998; makefile: 53; sh: 14
file content (172 lines) | stat: -rw-r--r-- 7,123 bytes parent folder | download | duplicates (2)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
from authlib.common.security import generate_token
from authlib.common.urls import add_params_to_uri
from authlib.consts import default_json_headers


class DeviceAuthorizationEndpoint:
    """This OAuth 2.0 [RFC6749] protocol extension enables OAuth clients to
    request user authorization from applications on devices that have
    limited input capabilities or lack a suitable browser.  Such devices
    include smart TVs, media consoles, picture frames, and printers,
    which lack an easy input method or a suitable browser required for
    traditional OAuth interactions. Here is the authorization flow::

        +----------+                                +----------------+
        |          |>---(A)-- Client Identifier --->|                |
        |          |                                |                |
        |          |<---(B)-- Device Code,      ---<|                |
        |          |          User Code,            |                |
        |  Device  |          & Verification URI    |                |
        |  Client  |                                |                |
        |          |  [polling]                     |                |
        |          |>---(E)-- Device Code       --->|                |
        |          |          & Client Identifier   |                |
        |          |                                |  Authorization |
        |          |<---(F)-- Access Token      ---<|     Server     |
        +----------+   (& Optional Refresh Token)   |                |
              v                                     |                |
              :                                     |                |
             (C) User Code & Verification URI       |                |
              :                                     |                |
              v                                     |                |
        +----------+                                |                |
        | End User |                                |                |
        |    at    |<---(D)-- End user reviews  --->|                |
        |  Browser |          authorization request |                |
        +----------+                                +----------------+

    This DeviceAuthorizationEndpoint is the implementation of step (A) and (B).

    (A) The client requests access from the authorization server and
        includes its client identifier in the request.

    (B) The authorization server issues a device code and an end-user
        code and provides the end-user verification URI.
    """

    ENDPOINT_NAME = "device_authorization"
    CLIENT_AUTH_METHODS = ["client_secret_basic", "client_secret_post", "none"]

    #: customize "user_code" type, string or digital
    USER_CODE_TYPE = "string"

    #: The lifetime in seconds of the "device_code" and "user_code"
    EXPIRES_IN = 1800

    #: The minimum amount of time in seconds that the client SHOULD
    #: wait between polling requests to the token endpoint.
    INTERVAL = 5

    def __init__(self, server):
        self.server = server

    def __call__(self, request):
        # make it callable for authorization server
        # ``create_endpoint_response``
        return self.create_endpoint_response(request)

    def create_endpoint_request(self, request):
        return self.server.create_oauth2_request(request)

    def authenticate_client(self, request):
        """client_id is REQUIRED **if the client is not** authenticating with the
        authorization server as described in Section 3.2.1. of [RFC6749].

        This means the endpoint support "none" authentication method. In this case,
        this endpoint's auth methods are:

        - client_secret_basic
        - client_secret_post
        - none

        Developers change the value of ``CLIENT_AUTH_METHODS`` in subclass. For
        instance::

            class MyDeviceAuthorizationEndpoint(DeviceAuthorizationEndpoint):
                # only support ``client_secret_basic`` auth method
                CLIENT_AUTH_METHODS = ["client_secret_basic"]
        """
        client = self.server.authenticate_client(
            request, self.CLIENT_AUTH_METHODS, self.ENDPOINT_NAME
        )
        request.client = client
        return client

    def create_endpoint_response(self, request):
        # https://tools.ietf.org/html/rfc8628#section-3.1

        self.authenticate_client(request)
        self.server.validate_requested_scope(request.payload.scope)

        device_code = self.generate_device_code()
        user_code = self.generate_user_code()
        verification_uri = self.get_verification_uri()
        verification_uri_complete = add_params_to_uri(
            verification_uri, [("user_code", user_code)]
        )

        data = {
            "device_code": device_code,
            "user_code": user_code,
            "verification_uri": verification_uri,
            "verification_uri_complete": verification_uri_complete,
            "expires_in": self.EXPIRES_IN,
            "interval": self.INTERVAL,
        }

        self.save_device_credential(
            request.payload.client_id, request.payload.scope, data
        )
        return 200, data, default_json_headers

    def generate_user_code(self):
        """A method to generate ``user_code`` value for device authorization
        endpoint. This method will generate a random string like MQNA-JPOZ.
        Developers can rewrite this  method to create their own ``user_code``.
        """
        # https://tools.ietf.org/html/rfc8628#section-6.1
        if self.USER_CODE_TYPE == "digital":
            return create_digital_user_code()
        return create_string_user_code()

    def generate_device_code(self):
        """A method to generate ``device_code`` value for device authorization
        endpoint. This method will generate a random string of 42 characters.
        Developers can rewrite this method to create their own ``device_code``.
        """
        return generate_token(42)

    def get_verification_uri(self):
        """Define the ``verification_uri`` of device authorization endpoint.
        Developers MUST implement this method in subclass::

            def get_verification_uri(self):
                return "https://your-company.com/active"
        """
        raise NotImplementedError()

    def save_device_credential(self, client_id, scope, data):
        """Save device token into database for later use. Developers MUST
        implement this method in subclass::

            def save_device_credential(self, client_id, scope, data):
                item = DeviceCredential(client_id=client_id, scope=scope, **data)
                item.save()
        """
        raise NotImplementedError()


def create_string_user_code():
    base = "BCDFGHJKLMNPQRSTVWXZ"
    return "-".join([generate_token(4, base), generate_token(4, base)])


def create_digital_user_code():
    base = "0123456789"
    return "-".join(
        [
            generate_token(3, base),
            generate_token(3, base),
            generate_token(3, base),
        ]
    )