171 lines
6.9 KiB
Python
171 lines
6.9 KiB
Python
from authlib.consts import default_json_headers
|
|
from authlib.common.security import generate_token
|
|
from authlib.common.urls import add_params_to_uri
|
|
|
|
|
|
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.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.client_id, request.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),
|
|
])
|