codekingpro/portable-devtools
114k
1from authlib.consts import default_json_headers2from authlib.common.security import generate_token3from authlib.common.urls import add_params_to_uri4 5 6class DeviceAuthorizationEndpoint:7 """This OAuth 2.0 [RFC6749] protocol extension enables OAuth clients to8 request user authorization from applications on devices that have9 limited input capabilities or lack a suitable browser. Such devices10 include smart TVs, media consoles, picture frames, and printers,11 which lack an easy input method or a suitable browser required for12 traditional OAuth interactions. Here is the authorization flow::13 14 +----------+ +----------------+15 | |>---(A)-- Client Identifier --->| |16 | | | |17 | |<---(B)-- Device Code, ---<| |18 | | User Code, | |19 | Device | & Verification URI | |20 | Client | | |21 | | [polling] | |22 | |>---(E)-- Device Code --->| |23 | | & Client Identifier | |24 | | | Authorization |25 | |<---(F)-- Access Token ---<| Server |26 +----------+ (& Optional Refresh Token) | |27 v | |28 : | |29 (C) User Code & Verification URI | |30 : | |31 v | |32 +----------+ | |33 | End User | | |34 | at |<---(D)-- End user reviews --->| |35 | Browser | authorization request | |36 +----------+ +----------------+37 38 This DeviceAuthorizationEndpoint is the implementation of step (A) and (B).39 40 (A) The client requests access from the authorization server and41 includes its client identifier in the request.42 43 (B) The authorization server issues a device code and an end-user44 code and provides the end-user verification URI.45 """46 47 ENDPOINT_NAME = 'device_authorization'48 CLIENT_AUTH_METHODS = ['client_secret_basic', 'client_secret_post', 'none']49 50 #: customize "user_code" type, string or digital51 USER_CODE_TYPE = 'string'52 53 #: The lifetime in seconds of the "device_code" and "user_code"54 EXPIRES_IN = 180055 56 #: The minimum amount of time in seconds that the client SHOULD57 #: wait between polling requests to the token endpoint.58 INTERVAL = 559 60 def __init__(self, server):61 self.server = server62 63 def __call__(self, request):64 # make it callable for authorization server65 # ``create_endpoint_response``66 return self.create_endpoint_response(request)67 68 def create_endpoint_request(self, request):69 return self.server.create_oauth2_request(request)70 71 def authenticate_client(self, request):72 """client_id is REQUIRED **if the client is not** authenticating with the73 authorization server as described in Section 3.2.1. of [RFC6749].74 75 This means the endpoint support "none" authentication method. In this case,76 this endpoint's auth methods are:77 78 - client_secret_basic79 - client_secret_post80 - none81 82 Developers change the value of ``CLIENT_AUTH_METHODS`` in subclass. For83 instance::84 85 class MyDeviceAuthorizationEndpoint(DeviceAuthorizationEndpoint):86 # only support ``client_secret_basic`` auth method87 CLIENT_AUTH_METHODS = ['client_secret_basic']88 """89 client = self.server.authenticate_client(90 request, self.CLIENT_AUTH_METHODS, self.ENDPOINT_NAME)91 request.client = client92 return client93 94 def create_endpoint_response(self, request):95 # https://tools.ietf.org/html/rfc8628#section-3.196 97 self.authenticate_client(request)98 self.server.validate_requested_scope(request.scope)99 100 device_code = self.generate_device_code()101 user_code = self.generate_user_code()102 verification_uri = self.get_verification_uri()103 verification_uri_complete = add_params_to_uri(104 verification_uri, [('user_code', user_code)])105 106 data = {107 'device_code': device_code,108 'user_code': user_code,109 'verification_uri': verification_uri,110 'verification_uri_complete': verification_uri_complete,111 'expires_in': self.EXPIRES_IN,112 'interval': self.INTERVAL,113 }114 115 self.save_device_credential(request.client_id, request.scope, data)116 return 200, data, default_json_headers117 118 def generate_user_code(self):119 """A method to generate ``user_code`` value for device authorization120 endpoint. This method will generate a random string like MQNA-JPOZ.121 Developers can rewrite this method to create their own ``user_code``.122 """123 # https://tools.ietf.org/html/rfc8628#section-6.1124 if self.USER_CODE_TYPE == 'digital':125 return create_digital_user_code()126 return create_string_user_code()127 128 def generate_device_code(self):129 """A method to generate ``device_code`` value for device authorization130 endpoint. This method will generate a random string of 42 characters.131 Developers can rewrite this method to create their own ``device_code``.132 """133 return generate_token(42)134 135 def get_verification_uri(self):136 """Define the ``verification_uri`` of device authorization endpoint.137 Developers MUST implement this method in subclass::138 139 def get_verification_uri(self):140 return 'https://your-company.com/active'141 """142 raise NotImplementedError()143 144 def save_device_credential(self, client_id, scope, data):145 """Save device token into database for later use. Developers MUST146 implement this method in subclass::147 148 def save_device_credential(self, client_id, scope, data):149 item = DeviceCredential(150 client_id=client_id,151 scope=scope,152 **data153 )154 item.save()155 """156 raise NotImplementedError()157 158 159def create_string_user_code():160 base = 'BCDFGHJKLMNPQRSTVWXZ'161 return '-'.join([generate_token(4, base), generate_token(4, base)])162 163 164def create_digital_user_code():165 base = '0123456789'166 return '-'.join([167 generate_token(3, base),168 generate_token(3, base),169 generate_token(3, base),170 ])171 