venv added, updated

This commit is contained in:
Norbert
2024-09-13 09:46:28 +02:00
parent 577596d9f3
commit 82af8c809a
4812 changed files with 640223 additions and 2 deletions

View File

@@ -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',
]

View File

@@ -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()

View File

@@ -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),
])

View File

@@ -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'

View File

@@ -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