venv added, updated
This commit is contained in:
@@ -0,0 +1,22 @@
|
||||
"""
|
||||
authlib.oauth2.rfc8628
|
||||
~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
This module represents an implementation of
|
||||
OAuth 2.0 Device Authorization Grant.
|
||||
|
||||
https://tools.ietf.org/html/rfc8628
|
||||
"""
|
||||
|
||||
from .endpoint import DeviceAuthorizationEndpoint
|
||||
from .device_code import DeviceCodeGrant, DEVICE_CODE_GRANT_TYPE
|
||||
from .models import DeviceCredentialMixin, DeviceCredentialDict
|
||||
from .errors import AuthorizationPendingError, SlowDownError, ExpiredTokenError
|
||||
|
||||
|
||||
__all__ = [
|
||||
'DeviceAuthorizationEndpoint',
|
||||
'DeviceCodeGrant', 'DEVICE_CODE_GRANT_TYPE',
|
||||
'DeviceCredentialMixin', 'DeviceCredentialDict',
|
||||
'AuthorizationPendingError', 'SlowDownError', 'ExpiredTokenError',
|
||||
]
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,183 @@
|
||||
import logging
|
||||
from ..rfc6749.errors import (
|
||||
InvalidRequestError,
|
||||
UnauthorizedClientError,
|
||||
AccessDeniedError,
|
||||
)
|
||||
from ..rfc6749 import BaseGrant, TokenEndpointMixin
|
||||
from .errors import (
|
||||
AuthorizationPendingError,
|
||||
ExpiredTokenError,
|
||||
SlowDownError,
|
||||
)
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
DEVICE_CODE_GRANT_TYPE = 'urn:ietf:params:oauth:grant-type:device_code'
|
||||
|
||||
|
||||
class DeviceCodeGrant(BaseGrant, TokenEndpointMixin):
|
||||
"""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 DeviceCodeGrant is the implementation of step (E) and (F).
|
||||
|
||||
(E) While the end user reviews the client's request (step D), the
|
||||
client repeatedly polls the authorization server to find out if
|
||||
the user completed the user authorization step. The client
|
||||
includes the device code and its client identifier.
|
||||
|
||||
(F) The authorization server validates the device code provided by
|
||||
the client and responds with the access token if the client is
|
||||
granted access, an error if they are denied access, or an
|
||||
indication that the client should continue to poll.
|
||||
"""
|
||||
GRANT_TYPE = DEVICE_CODE_GRANT_TYPE
|
||||
TOKEN_ENDPOINT_AUTH_METHODS = ['client_secret_basic', 'client_secret_post', 'none']
|
||||
|
||||
def validate_token_request(self):
|
||||
"""After displaying instructions to the user, the client creates an
|
||||
access token request and sends it to the token endpoint with the
|
||||
following parameters:
|
||||
|
||||
grant_type
|
||||
REQUIRED. Value MUST be set to
|
||||
"urn:ietf:params:oauth:grant-type:device_code".
|
||||
|
||||
device_code
|
||||
REQUIRED. The device verification code, "device_code" from the
|
||||
device authorization response.
|
||||
|
||||
client_id
|
||||
REQUIRED if the client is not authenticating with the
|
||||
authorization server as described in Section 3.2.1. of [RFC6749].
|
||||
The client identifier as described in Section 2.2 of [RFC6749].
|
||||
|
||||
For example, the client makes the following HTTPS request::
|
||||
|
||||
POST /token HTTP/1.1
|
||||
Host: server.example.com
|
||||
Content-Type: application/x-www-form-urlencoded
|
||||
|
||||
grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Adevice_code
|
||||
&device_code=GmRhmhcxhwAzkoEqiMEg_DnyEysNkuNhszIySk9eS
|
||||
&client_id=1406020730
|
||||
"""
|
||||
device_code = self.request.data.get('device_code')
|
||||
if not device_code:
|
||||
raise InvalidRequestError('Missing "device_code" in payload')
|
||||
|
||||
client = self.authenticate_token_endpoint_client()
|
||||
if not client.check_grant_type(self.GRANT_TYPE):
|
||||
raise UnauthorizedClientError()
|
||||
|
||||
credential = self.query_device_credential(device_code)
|
||||
if not credential:
|
||||
raise InvalidRequestError('Invalid "device_code" in payload')
|
||||
|
||||
if credential.get_client_id() != client.get_client_id():
|
||||
raise UnauthorizedClientError()
|
||||
|
||||
user = self.validate_device_credential(credential)
|
||||
self.request.user = user
|
||||
self.request.client = client
|
||||
self.request.credential = credential
|
||||
|
||||
def create_token_response(self):
|
||||
"""If the access token request is valid and authorized, the
|
||||
authorization server issues an access token and optional refresh
|
||||
token.
|
||||
"""
|
||||
client = self.request.client
|
||||
scope = self.request.credential.get_scope()
|
||||
token = self.generate_token(
|
||||
user=self.request.user,
|
||||
scope=scope,
|
||||
include_refresh_token=client.check_grant_type('refresh_token'),
|
||||
)
|
||||
log.debug('Issue token %r to %r', token, client)
|
||||
self.save_token(token)
|
||||
self.execute_hook('process_token', token=token)
|
||||
return 200, token, self.TOKEN_RESPONSE_HEADER
|
||||
|
||||
def validate_device_credential(self, credential):
|
||||
if credential.is_expired():
|
||||
raise ExpiredTokenError()
|
||||
|
||||
user_code = credential.get_user_code()
|
||||
user_grant = self.query_user_grant(user_code)
|
||||
|
||||
if user_grant is not None:
|
||||
user, approved = user_grant
|
||||
if not approved:
|
||||
raise AccessDeniedError()
|
||||
return user
|
||||
|
||||
if self.should_slow_down(credential):
|
||||
raise SlowDownError()
|
||||
|
||||
raise AuthorizationPendingError()
|
||||
|
||||
def query_device_credential(self, device_code):
|
||||
"""Get device credential from previously savings via ``DeviceAuthorizationEndpoint``.
|
||||
Developers MUST implement it in subclass::
|
||||
|
||||
def query_device_credential(self, device_code):
|
||||
return DeviceCredential.get(device_code)
|
||||
|
||||
:param device_code: a string represent the code.
|
||||
:return: DeviceCredential instance
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
def query_user_grant(self, user_code):
|
||||
"""Get user and grant via the given user code. Developers MUST
|
||||
implement it in subclass::
|
||||
|
||||
def query_user_grant(self, user_code):
|
||||
# e.g. we saved user grant info in redis
|
||||
data = redis.get('oauth_user_grant:' + user_code)
|
||||
if not data:
|
||||
return None
|
||||
|
||||
user_id, allowed = data.split()
|
||||
user = User.get(user_id)
|
||||
return user, bool(allowed)
|
||||
|
||||
Note, user grant information is saved by verification endpoint.
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
def should_slow_down(self, credential):
|
||||
"""The authorization request is still pending and polling should
|
||||
continue, but the interval MUST be increased by 5 seconds for this
|
||||
and all subsequent requests.
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
@@ -0,0 +1,170 @@
|
||||
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),
|
||||
])
|
||||
@@ -0,0 +1,27 @@
|
||||
from ..rfc6749.errors import OAuth2Error
|
||||
|
||||
# https://tools.ietf.org/html/rfc8628#section-3.5
|
||||
|
||||
|
||||
class AuthorizationPendingError(OAuth2Error):
|
||||
"""The authorization request is still pending as the end user hasn't
|
||||
yet completed the user-interaction steps (Section 3.3).
|
||||
"""
|
||||
error = 'authorization_pending'
|
||||
|
||||
|
||||
class SlowDownError(OAuth2Error):
|
||||
"""A variant of "authorization_pending", the authorization request is
|
||||
still pending and polling should continue, but the interval MUST
|
||||
be increased by 5 seconds for this and all subsequent requests.
|
||||
"""
|
||||
error = 'slow_down'
|
||||
|
||||
|
||||
class ExpiredTokenError(OAuth2Error):
|
||||
"""The "device_code" has expired, and the device authorization
|
||||
session has concluded. The client MAY commence a new device
|
||||
authorization request but SHOULD wait for user interaction before
|
||||
restarting to avoid unnecessary polling.
|
||||
"""
|
||||
error = 'expired_token'
|
||||
@@ -0,0 +1,38 @@
|
||||
import time
|
||||
|
||||
|
||||
class DeviceCredentialMixin:
|
||||
def get_client_id(self):
|
||||
raise NotImplementedError()
|
||||
|
||||
def get_scope(self):
|
||||
raise NotImplementedError()
|
||||
|
||||
def get_user_code(self):
|
||||
raise NotImplementedError()
|
||||
|
||||
def is_expired(self):
|
||||
raise NotImplementedError()
|
||||
|
||||
|
||||
class DeviceCredentialDict(dict, DeviceCredentialMixin):
|
||||
def get_client_id(self):
|
||||
return self['client_id']
|
||||
|
||||
def get_scope(self):
|
||||
return self.get('scope')
|
||||
|
||||
def get_user_code(self):
|
||||
return self['user_code']
|
||||
|
||||
def get_nonce(self):
|
||||
return self.get('nonce')
|
||||
|
||||
def get_auth_time(self):
|
||||
return self.get('auth_time')
|
||||
|
||||
def is_expired(self):
|
||||
expires_at = self.get('expires_at')
|
||||
if expires_at:
|
||||
return expires_at < time.time()
|
||||
return False
|
||||
Reference in New Issue
Block a user