Bump requests-oauthlib from 1.3.1 to 2.0.0 (#2293)

* Bump requests-oauthlib from 1.3.1 to 2.0.0

Bumps [requests-oauthlib](https://github.com/requests/requests-oauthlib) from 1.3.1 to 2.0.0.
- [Release notes](https://github.com/requests/requests-oauthlib/releases)
- [Changelog](https://github.com/requests/requests-oauthlib/blob/master/HISTORY.rst)
- [Commits](https://github.com/requests/requests-oauthlib/compare/v1.3.1...v2.0.0)

---
updated-dependencies:
- dependency-name: requests-oauthlib
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>

* Update requests-oauthlib==2.0.0

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: JonnyWong16 <9099342+JonnyWong16@users.noreply.github.com>

[skip ci]
This commit is contained in:
dependabot[bot] 2024-03-30 15:28:02 -07:00 committed by GitHub
parent 452a4afdcf
commit 0d1d2a3e6b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
60 changed files with 2414 additions and 2291 deletions

View file

@ -0,0 +1,4 @@
from .cli import cli_detect
if __name__ == "__main__":
cli_detect()

File diff suppressed because it is too large Load diff

View file

@ -4,8 +4,13 @@ from collections import Counter
from functools import lru_cache from functools import lru_cache
from typing import Counter as TypeCounter, Dict, List, Optional, Tuple from typing import Counter as TypeCounter, Dict, List, Optional, Tuple
from .assets import FREQUENCIES from .constant import (
from .constant import KO_NAMES, LANGUAGE_SUPPORTED_COUNT, TOO_SMALL_SEQUENCE, ZH_NAMES FREQUENCIES,
KO_NAMES,
LANGUAGE_SUPPORTED_COUNT,
TOO_SMALL_SEQUENCE,
ZH_NAMES,
)
from .md import is_suspiciously_successive_range from .md import is_suspiciously_successive_range
from .models import CoherenceMatches from .models import CoherenceMatches
from .utils import ( from .utils import (

View file

@ -0,0 +1,6 @@
from .__main__ import cli_detect, query_yes_no
__all__ = (
"cli_detect",
"query_yes_no",
)

File diff suppressed because it is too large Load diff

View file

@ -9,7 +9,8 @@ from .constant import (
) )
from .utils import ( from .utils import (
is_accentuated, is_accentuated,
is_ascii, is_arabic,
is_arabic_isolated_form,
is_case_variable, is_case_variable,
is_cjk, is_cjk,
is_emoticon, is_emoticon,
@ -128,8 +129,9 @@ class TooManyAccentuatedPlugin(MessDetectorPlugin):
@property @property
def ratio(self) -> float: def ratio(self) -> float:
if self._character_count == 0 or self._character_count < 8: if self._character_count < 8:
return 0.0 return 0.0
ratio_of_accentuation: float = self._accentuated_count / self._character_count ratio_of_accentuation: float = self._accentuated_count / self._character_count
return ratio_of_accentuation if ratio_of_accentuation >= 0.35 else 0.0 return ratio_of_accentuation if ratio_of_accentuation >= 0.35 else 0.0
@ -234,16 +236,13 @@ class SuspiciousRange(MessDetectorPlugin):
@property @property
def ratio(self) -> float: def ratio(self) -> float:
if self._character_count == 0: if self._character_count <= 24:
return 0.0 return 0.0
ratio_of_suspicious_range_usage: float = ( ratio_of_suspicious_range_usage: float = (
self._suspicious_successive_range_count * 2 self._suspicious_successive_range_count * 2
) / self._character_count ) / self._character_count
if ratio_of_suspicious_range_usage < 0.1:
return 0.0
return ratio_of_suspicious_range_usage return ratio_of_suspicious_range_usage
@ -296,7 +295,11 @@ class SuperWeirdWordPlugin(MessDetectorPlugin):
self._is_current_word_bad = True self._is_current_word_bad = True
# Word/Buffer ending with an upper case accentuated letter are so rare, # Word/Buffer ending with an upper case accentuated letter are so rare,
# that we will consider them all as suspicious. Same weight as foreign_long suspicious. # that we will consider them all as suspicious. Same weight as foreign_long suspicious.
if is_accentuated(self._buffer[-1]) and self._buffer[-1].isupper(): if (
is_accentuated(self._buffer[-1])
and self._buffer[-1].isupper()
and all(_.isupper() for _ in self._buffer) is False
):
self._foreign_long_count += 1 self._foreign_long_count += 1
self._is_current_word_bad = True self._is_current_word_bad = True
if buffer_length >= 24 and self._foreign_long_watch: if buffer_length >= 24 and self._foreign_long_watch:
@ -419,7 +422,7 @@ class ArchaicUpperLowerPlugin(MessDetectorPlugin):
return return
if self._current_ascii_only is True and is_ascii(character) is False: if self._current_ascii_only is True and character.isascii() is False:
self._current_ascii_only = False self._current_ascii_only = False
if self._last_alpha_seen is not None: if self._last_alpha_seen is not None:
@ -455,6 +458,34 @@ class ArchaicUpperLowerPlugin(MessDetectorPlugin):
return self._successive_upper_lower_count_final / self._character_count return self._successive_upper_lower_count_final / self._character_count
class ArabicIsolatedFormPlugin(MessDetectorPlugin):
def __init__(self) -> None:
self._character_count: int = 0
self._isolated_form_count: int = 0
def reset(self) -> None: # pragma: no cover
self._character_count = 0
self._isolated_form_count = 0
def eligible(self, character: str) -> bool:
return is_arabic(character)
def feed(self, character: str) -> None:
self._character_count += 1
if is_arabic_isolated_form(character):
self._isolated_form_count += 1
@property
def ratio(self) -> float:
if self._character_count < 8:
return 0.0
isolated_form_usage: float = self._isolated_form_count / self._character_count
return isolated_form_usage
@lru_cache(maxsize=1024) @lru_cache(maxsize=1024)
def is_suspiciously_successive_range( def is_suspiciously_successive_range(
unicode_range_a: Optional[str], unicode_range_b: Optional[str] unicode_range_a: Optional[str], unicode_range_b: Optional[str]
@ -522,6 +553,8 @@ def is_suspiciously_successive_range(
return False return False
if "Forms" in unicode_range_a or "Forms" in unicode_range_b: if "Forms" in unicode_range_a or "Forms" in unicode_range_b:
return False return False
if unicode_range_a == "Basic Latin" or unicode_range_b == "Basic Latin":
return False
return True return True

View file

@ -54,16 +54,19 @@ class CharsetMatch:
# Below 1% difference --> Use Coherence # Below 1% difference --> Use Coherence
if chaos_difference < 0.01 and coherence_difference > 0.02: if chaos_difference < 0.01 and coherence_difference > 0.02:
# When having a tough decision, use the result that decoded as many multi-byte as possible.
if chaos_difference == 0.0 and self.coherence == other.coherence:
return self.multi_byte_usage > other.multi_byte_usage
return self.coherence > other.coherence return self.coherence > other.coherence
elif chaos_difference < 0.01 and coherence_difference <= 0.02:
# When having a difficult decision, use the result that decoded as many multi-byte as possible.
# preserve RAM usage!
if len(self._payload) >= TOO_BIG_SEQUENCE:
return self.chaos < other.chaos
return self.multi_byte_usage > other.multi_byte_usage
return self.chaos < other.chaos return self.chaos < other.chaos
@property @property
def multi_byte_usage(self) -> float: def multi_byte_usage(self) -> float:
return 1.0 - len(str(self)) / len(self.raw) return 1.0 - (len(str(self)) / len(self.raw))
def __str__(self) -> str: def __str__(self) -> str:
# Lazy Str Loading # Lazy Str Loading

View file

@ -32,6 +32,8 @@ def is_accentuated(character: str) -> bool:
or "WITH DIAERESIS" in description or "WITH DIAERESIS" in description
or "WITH CIRCUMFLEX" in description or "WITH CIRCUMFLEX" in description
or "WITH TILDE" in description or "WITH TILDE" in description
or "WITH MACRON" in description
or "WITH RING ABOVE" in description
) )
@ -69,15 +71,6 @@ def is_latin(character: str) -> bool:
return "LATIN" in description return "LATIN" in description
@lru_cache(maxsize=UTF8_MAXIMAL_ALLOCATION)
def is_ascii(character: str) -> bool:
try:
character.encode("ascii")
except UnicodeEncodeError:
return False
return True
@lru_cache(maxsize=UTF8_MAXIMAL_ALLOCATION) @lru_cache(maxsize=UTF8_MAXIMAL_ALLOCATION)
def is_punctuation(character: str) -> bool: def is_punctuation(character: str) -> bool:
character_category: str = unicodedata.category(character) character_category: str = unicodedata.category(character)
@ -105,7 +98,7 @@ def is_symbol(character: str) -> bool:
if character_range is None: if character_range is None:
return False return False
return "Forms" in character_range return "Forms" in character_range and character_category != "Lo"
@lru_cache(maxsize=UTF8_MAXIMAL_ALLOCATION) @lru_cache(maxsize=UTF8_MAXIMAL_ALLOCATION)
@ -115,7 +108,7 @@ def is_emoticon(character: str) -> bool:
if character_range is None: if character_range is None:
return False return False
return "Emoticons" in character_range return "Emoticons" in character_range or "Pictographs" in character_range
@lru_cache(maxsize=UTF8_MAXIMAL_ALLOCATION) @lru_cache(maxsize=UTF8_MAXIMAL_ALLOCATION)
@ -133,12 +126,6 @@ def is_case_variable(character: str) -> bool:
return character.islower() != character.isupper() return character.islower() != character.isupper()
def is_private_use_only(character: str) -> bool:
character_category: str = unicodedata.category(character)
return character_category == "Co"
@lru_cache(maxsize=UTF8_MAXIMAL_ALLOCATION) @lru_cache(maxsize=UTF8_MAXIMAL_ALLOCATION)
def is_cjk(character: str) -> bool: def is_cjk(character: str) -> bool:
try: try:
@ -189,6 +176,26 @@ def is_thai(character: str) -> bool:
return "THAI" in character_name return "THAI" in character_name
@lru_cache(maxsize=UTF8_MAXIMAL_ALLOCATION)
def is_arabic(character: str) -> bool:
try:
character_name = unicodedata.name(character)
except ValueError:
return False
return "ARABIC" in character_name
@lru_cache(maxsize=UTF8_MAXIMAL_ALLOCATION)
def is_arabic_isolated_form(character: str) -> bool:
try:
character_name = unicodedata.name(character)
except ValueError:
return False
return "ARABIC" in character_name and "ISOLATED FORM" in character_name
@lru_cache(maxsize=len(UNICODE_RANGES_COMBINED)) @lru_cache(maxsize=len(UNICODE_RANGES_COMBINED))
def is_unicode_range_secondary(range_name: str) -> bool: def is_unicode_range_secondary(range_name: str) -> bool:
return any(keyword in range_name for keyword in UNICODE_SECONDARY_RANGE_KEYWORD) return any(keyword in range_name for keyword in UNICODE_SECONDARY_RANGE_KEYWORD)
@ -205,7 +212,7 @@ def is_unprintable(character: str) -> bool:
) )
def any_specified_encoding(sequence: bytes, search_zone: int = 4096) -> Optional[str]: def any_specified_encoding(sequence: bytes, search_zone: int = 8192) -> Optional[str]:
""" """
Extract using ASCII-only decoder any specified encoding in the first n-bytes. Extract using ASCII-only decoder any specified encoding in the first n-bytes.
""" """

View file

@ -2,5 +2,5 @@
Expose version Expose version
""" """
__version__ = "3.2.0" __version__ = "3.3.2"
VERSION = __version__.split(".") VERSION = __version__.split(".")

View file

@ -1,7 +1,7 @@
from .core import encode, decode, alabel, ulabel, IDNAError from .core import encode, decode, alabel, ulabel, IDNAError
import codecs import codecs
import re import re
from typing import Tuple, Optional from typing import Any, Tuple, Optional
_unicode_dots_re = re.compile('[\u002e\u3002\uff0e\uff61]') _unicode_dots_re = re.compile('[\u002e\u3002\uff0e\uff61]')
@ -26,24 +26,24 @@ class Codec(codecs.Codec):
return decode(data), len(data) return decode(data), len(data)
class IncrementalEncoder(codecs.BufferedIncrementalEncoder): class IncrementalEncoder(codecs.BufferedIncrementalEncoder):
def _buffer_encode(self, data: str, errors: str, final: bool) -> Tuple[str, int]: # type: ignore def _buffer_encode(self, data: str, errors: str, final: bool) -> Tuple[bytes, int]:
if errors != 'strict': if errors != 'strict':
raise IDNAError('Unsupported error handling \"{}\"'.format(errors)) raise IDNAError('Unsupported error handling \"{}\"'.format(errors))
if not data: if not data:
return "", 0 return b'', 0
labels = _unicode_dots_re.split(data) labels = _unicode_dots_re.split(data)
trailing_dot = '' trailing_dot = b''
if labels: if labels:
if not labels[-1]: if not labels[-1]:
trailing_dot = '.' trailing_dot = b'.'
del labels[-1] del labels[-1]
elif not final: elif not final:
# Keep potentially unfinished label until the next call # Keep potentially unfinished label until the next call
del labels[-1] del labels[-1]
if labels: if labels:
trailing_dot = '.' trailing_dot = b'.'
result = [] result = []
size = 0 size = 0
@ -54,18 +54,21 @@ class IncrementalEncoder(codecs.BufferedIncrementalEncoder):
size += len(label) size += len(label)
# Join with U+002E # Join with U+002E
result_str = '.'.join(result) + trailing_dot # type: ignore result_bytes = b'.'.join(result) + trailing_dot
size += len(trailing_dot) size += len(trailing_dot)
return result_str, size return result_bytes, size
class IncrementalDecoder(codecs.BufferedIncrementalDecoder): class IncrementalDecoder(codecs.BufferedIncrementalDecoder):
def _buffer_decode(self, data: str, errors: str, final: bool) -> Tuple[str, int]: # type: ignore def _buffer_decode(self, data: Any, errors: str, final: bool) -> Tuple[str, int]:
if errors != 'strict': if errors != 'strict':
raise IDNAError('Unsupported error handling \"{}\"'.format(errors)) raise IDNAError('Unsupported error handling \"{}\"'.format(errors))
if not data: if not data:
return ('', 0) return ('', 0)
if not isinstance(data, str):
data = str(data, 'ascii')
labels = _unicode_dots_re.split(data) labels = _unicode_dots_re.split(data)
trailing_dot = '' trailing_dot = ''
if labels: if labels:
@ -99,14 +102,17 @@ class StreamReader(Codec, codecs.StreamReader):
pass pass
def getregentry() -> codecs.CodecInfo: def search_function(name: str) -> Optional[codecs.CodecInfo]:
# Compatibility as a search_function for codecs.register() if name != 'idna2008':
return None
return codecs.CodecInfo( return codecs.CodecInfo(
name='idna', name=name,
encode=Codec().encode, # type: ignore encode=Codec().encode,
decode=Codec().decode, # type: ignore decode=Codec().decode,
incrementalencoder=IncrementalEncoder, incrementalencoder=IncrementalEncoder,
incrementaldecoder=IncrementalDecoder, incrementaldecoder=IncrementalDecoder,
streamwriter=StreamWriter, streamwriter=StreamWriter,
streamreader=StreamReader, streamreader=StreamReader,
) )
codecs.register(search_function)

View file

@ -318,7 +318,7 @@ def uts46_remap(domain: str, std3_rules: bool = True, transitional: bool = False
status = uts46row[1] status = uts46row[1]
replacement = None # type: Optional[str] replacement = None # type: Optional[str]
if len(uts46row) == 3: if len(uts46row) == 3:
replacement = uts46row[2] # type: ignore replacement = uts46row[2]
if (status == 'V' or if (status == 'V' or
(status == 'D' and not transitional) or (status == 'D' and not transitional) or
(status == '3' and not std3_rules and replacement is None)): (status == '3' and not std3_rules and replacement is None)):
@ -338,9 +338,9 @@ def uts46_remap(domain: str, std3_rules: bool = True, transitional: bool = False
def encode(s: Union[str, bytes, bytearray], strict: bool = False, uts46: bool = False, std3_rules: bool = False, transitional: bool = False) -> bytes: def encode(s: Union[str, bytes, bytearray], strict: bool = False, uts46: bool = False, std3_rules: bool = False, transitional: bool = False) -> bytes:
if isinstance(s, (bytes, bytearray)): if not isinstance(s, str):
try: try:
s = s.decode('ascii') s = str(s, 'ascii')
except UnicodeDecodeError: except UnicodeDecodeError:
raise IDNAError('should pass a unicode string to the function rather than a byte string.') raise IDNAError('should pass a unicode string to the function rather than a byte string.')
if uts46: if uts46:
@ -372,8 +372,8 @@ def encode(s: Union[str, bytes, bytearray], strict: bool = False, uts46: bool =
def decode(s: Union[str, bytes, bytearray], strict: bool = False, uts46: bool = False, std3_rules: bool = False) -> str: def decode(s: Union[str, bytes, bytearray], strict: bool = False, uts46: bool = False, std3_rules: bool = False) -> str:
try: try:
if isinstance(s, (bytes, bytearray)): if not isinstance(s, str):
s = s.decode('ascii') s = str(s, 'ascii')
except UnicodeDecodeError: except UnicodeDecodeError:
raise IDNAError('Invalid ASCII in A-label') raise IDNAError('Invalid ASCII in A-label')
if uts46: if uts46:

View file

@ -1,6 +1,6 @@
# This file is automatically generated by tools/idna-data # This file is automatically generated by tools/idna-data
__version__ = '15.0.0' __version__ = '15.1.0'
scripts = { scripts = {
'Greek': ( 'Greek': (
0x37000000374, 0x37000000374,
@ -59,6 +59,7 @@ scripts = {
0x2b7400002b81e, 0x2b7400002b81e,
0x2b8200002cea2, 0x2b8200002cea2,
0x2ceb00002ebe1, 0x2ceb00002ebe1,
0x2ebf00002ee5e,
0x2f8000002fa1e, 0x2f8000002fa1e,
0x300000003134b, 0x300000003134b,
0x31350000323b0, 0x31350000323b0,
@ -1834,7 +1835,6 @@ codepoint_classes = {
0xa7d50000a7d6, 0xa7d50000a7d6,
0xa7d70000a7d8, 0xa7d70000a7d8,
0xa7d90000a7da, 0xa7d90000a7da,
0xa7f20000a7f5,
0xa7f60000a7f8, 0xa7f60000a7f8,
0xa7fa0000a828, 0xa7fa0000a828,
0xa82c0000a82d, 0xa82c0000a82d,
@ -1907,9 +1907,7 @@ codepoint_classes = {
0x1060000010737, 0x1060000010737,
0x1074000010756, 0x1074000010756,
0x1076000010768, 0x1076000010768,
0x1078000010786, 0x1078000010781,
0x10787000107b1,
0x107b2000107bb,
0x1080000010806, 0x1080000010806,
0x1080800010809, 0x1080800010809,
0x1080a00010836, 0x1080a00010836,
@ -2134,6 +2132,7 @@ codepoint_classes = {
0x2b7400002b81e, 0x2b7400002b81e,
0x2b8200002cea2, 0x2b8200002cea2,
0x2ceb00002ebe1, 0x2ceb00002ebe1,
0x2ebf00002ee5e,
0x300000003134b, 0x300000003134b,
0x31350000323b0, 0x31350000323b0,
), ),

View file

@ -1,2 +1,2 @@
__version__ = '3.4' __version__ = '3.6'

File diff suppressed because it is too large Load diff

View file

@ -12,7 +12,7 @@ import logging
from logging import NullHandler from logging import NullHandler
__author__ = 'The OAuthlib Community' __author__ = 'The OAuthlib Community'
__version__ = '3.2.0' __version__ = '3.2.2'
logging.getLogger('oauthlib').addHandler(NullHandler()) logging.getLogger('oauthlib').addHandler(NullHandler())

View file

@ -18,11 +18,9 @@ from urllib.parse import (
from . import get_debug from . import get_debug
try: try:
from secrets import randbits from secrets import SystemRandom, randbits
from secrets import SystemRandom
except ImportError: except ImportError:
from random import getrandbits as randbits from random import SystemRandom, getrandbits as randbits
from random import SystemRandom
UNICODE_ASCII_CHARACTER_SET = ('abcdefghijklmnopqrstuvwxyz' UNICODE_ASCII_CHARACTER_SET = ('abcdefghijklmnopqrstuvwxyz'
'ABCDEFGHIJKLMNOPQRSTUVWXYZ' 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'

View file

@ -5,24 +5,19 @@ oauthlib.oauth1
This module is a wrapper for the most recent implementation of OAuth 1.0 Client This module is a wrapper for the most recent implementation of OAuth 1.0 Client
and Server classes. and Server classes.
""" """
from .rfc5849 import Client from .rfc5849 import (
from .rfc5849 import (SIGNATURE_HMAC, SIGNATURE_HMAC, SIGNATURE_HMAC_SHA1, SIGNATURE_HMAC_SHA256,
SIGNATURE_HMAC_SHA1, SIGNATURE_HMAC_SHA512, SIGNATURE_PLAINTEXT, SIGNATURE_RSA,
SIGNATURE_HMAC_SHA256, SIGNATURE_RSA_SHA1, SIGNATURE_RSA_SHA256, SIGNATURE_RSA_SHA512,
SIGNATURE_HMAC_SHA512, SIGNATURE_TYPE_AUTH_HEADER, SIGNATURE_TYPE_BODY, SIGNATURE_TYPE_QUERY,
SIGNATURE_RSA, Client,
SIGNATURE_RSA_SHA1, )
SIGNATURE_RSA_SHA256, from .rfc5849.endpoints import (
SIGNATURE_RSA_SHA512, AccessTokenEndpoint, AuthorizationEndpoint, RequestTokenEndpoint,
SIGNATURE_PLAINTEXT) ResourceEndpoint, SignatureOnlyEndpoint, WebApplicationServer,
from .rfc5849 import SIGNATURE_TYPE_AUTH_HEADER, SIGNATURE_TYPE_QUERY )
from .rfc5849 import SIGNATURE_TYPE_BODY from .rfc5849.errors import (
InsecureTransportError, InvalidClientError, InvalidRequestError,
InvalidSignatureMethodError, OAuth1Error,
)
from .rfc5849.request_validator import RequestValidator from .rfc5849.request_validator import RequestValidator
from .rfc5849.endpoints import RequestTokenEndpoint, AuthorizationEndpoint
from .rfc5849.endpoints import AccessTokenEndpoint, ResourceEndpoint
from .rfc5849.endpoints import SignatureOnlyEndpoint, WebApplicationServer
from .rfc5849.errors import (InsecureTransportError,
InvalidClientError,
InvalidRequestError,
InvalidSignatureMethodError,
OAuth1Error)

View file

@ -11,12 +11,11 @@ import time
from oauthlib.common import CaseInsensitiveDict, Request, generate_token from oauthlib.common import CaseInsensitiveDict, Request, generate_token
from .. import ( from .. import (
CONTENT_TYPE_FORM_URLENCODED, CONTENT_TYPE_FORM_URLENCODED, SIGNATURE_HMAC_SHA1, SIGNATURE_HMAC_SHA256,
SIGNATURE_HMAC_SHA1, SIGNATURE_HMAC_SHA256, SIGNATURE_HMAC_SHA512, SIGNATURE_HMAC_SHA512, SIGNATURE_PLAINTEXT, SIGNATURE_RSA_SHA1,
SIGNATURE_RSA_SHA1, SIGNATURE_RSA_SHA256, SIGNATURE_RSA_SHA512, SIGNATURE_RSA_SHA256, SIGNATURE_RSA_SHA512, SIGNATURE_TYPE_AUTH_HEADER,
SIGNATURE_PLAINTEXT, SIGNATURE_TYPE_BODY, SIGNATURE_TYPE_QUERY, errors, signature, utils,
SIGNATURE_TYPE_AUTH_HEADER, SIGNATURE_TYPE_BODY, )
SIGNATURE_TYPE_QUERY, errors, signature, utils)
class BaseEndpoint: class BaseEndpoint:

View file

@ -152,7 +152,7 @@ class RequestTokenEndpoint(BaseEndpoint):
request.client_key = self.request_validator.dummy_client request.client_key = self.request_validator.dummy_client
# Note that `realm`_ is only used in authorization headers and how # Note that `realm`_ is only used in authorization headers and how
# it should be interepreted is not included in the OAuth spec. # it should be interpreted is not included in the OAuth spec.
# However they could be seen as a scope or realm to which the # However they could be seen as a scope or realm to which the
# client has access and as such every client should be checked # client has access and as such every client should be checked
# to ensure it is authorized access to that scope or realm. # to ensure it is authorized access to that scope or realm.
@ -164,7 +164,7 @@ class RequestTokenEndpoint(BaseEndpoint):
# workflow where a client requests access to a specific realm. # workflow where a client requests access to a specific realm.
# This first step (obtaining request token) need not require a realm # This first step (obtaining request token) need not require a realm
# and can then be identified by checking the require_resource_owner # and can then be identified by checking the require_resource_owner
# flag and abscence of realm. # flag and absence of realm.
# #
# Clients obtaining an access token will not supply a realm and it will # Clients obtaining an access token will not supply a realm and it will
# not be checked. Instead the previously requested realm should be # not be checked. Instead the previously requested realm should be

View file

@ -113,7 +113,7 @@ class ResourceEndpoint(BaseEndpoint):
request.resource_owner_key = self.request_validator.dummy_access_token request.resource_owner_key = self.request_validator.dummy_access_token
# Note that `realm`_ is only used in authorization headers and how # Note that `realm`_ is only used in authorization headers and how
# it should be interepreted is not included in the OAuth spec. # it should be interpreted is not included in the OAuth spec.
# However they could be seen as a scope or realm to which the # However they could be seen as a scope or realm to which the
# client has access and as such every client should be checked # client has access and as such every client should be checked
# to ensure it is authorized access to that scope or realm. # to ensure it is authorized access to that scope or realm.
@ -125,7 +125,7 @@ class ResourceEndpoint(BaseEndpoint):
# workflow where a client requests access to a specific realm. # workflow where a client requests access to a specific realm.
# This first step (obtaining request token) need not require a realm # This first step (obtaining request token) need not require a realm
# and can then be identified by checking the require_resource_owner # and can then be identified by checking the require_resource_owner
# flag and abscence of realm. # flag and absence of realm.
# #
# Clients obtaining an access token will not supply a realm and it will # Clients obtaining an access token will not supply a realm and it will
# not be checked. Instead the previously requested realm should be # not be checked. Instead the previously requested realm should be

View file

@ -19,7 +19,7 @@ class RequestValidator:
Methods used to check the format of input parameters. Common tests include Methods used to check the format of input parameters. Common tests include
length, character set, membership, range or pattern. These tests are length, character set, membership, range or pattern. These tests are
referred to as `whitelisting or blacklisting`_. Whitelisting is better referred to as `whitelisting or blacklisting`_. Whitelisting is better
but blacklisting can be usefull to spot malicious activity. but blacklisting can be useful to spot malicious activity.
The following have methods a default implementation: The following have methods a default implementation:
- check_client_key - check_client_key
@ -443,7 +443,7 @@ class RequestValidator:
:type request: oauthlib.common.Request :type request: oauthlib.common.Request
:returns: None :returns: None
Per `Section 2.3`__ of the spec: Per `Section 2.3`_ of the spec:
"The server MUST (...) ensure that the temporary "The server MUST (...) ensure that the temporary
credentials have not expired or been used before." credentials have not expired or been used before."
@ -831,7 +831,7 @@ class RequestValidator:
"""Associate an authorization verifier with a request token. """Associate an authorization verifier with a request token.
:param token: A request token string. :param token: A request token string.
:param verifier A dictionary containing the oauth_verifier and :param verifier: A dictionary containing the oauth_verifier and
oauth_token oauth_token
:param request: OAuthlib request. :param request: OAuthlib request.
:type request: oauthlib.common.Request :type request: oauthlib.common.Request

View file

@ -37,15 +37,15 @@ should have no impact on properly behaving programs.
import binascii import binascii
import hashlib import hashlib
import hmac import hmac
import ipaddress
import logging import logging
import urllib.parse as urlparse
import warnings import warnings
from oauthlib.common import extract_params, safe_string_equals, urldecode from oauthlib.common import extract_params, safe_string_equals, urldecode
import urllib.parse as urlparse
from . import utils from . import utils
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
@ -131,7 +131,12 @@ def base_string_uri(uri: str, host: str = None) -> str:
raise ValueError('uri must be a string.') raise ValueError('uri must be a string.')
# FIXME: urlparse does not support unicode # FIXME: urlparse does not support unicode
scheme, netloc, path, params, query, fragment = urlparse.urlparse(uri) output = urlparse.urlparse(uri)
scheme = output.scheme
hostname = output.hostname
port = output.port
path = output.path
params = output.params
# The scheme, authority, and path of the request resource URI `RFC3986` # The scheme, authority, and path of the request resource URI `RFC3986`
# are included by constructing an "http" or "https" URI representing # are included by constructing an "http" or "https" URI representing
@ -153,13 +158,22 @@ def base_string_uri(uri: str, host: str = None) -> str:
# 1. The scheme and host MUST be in lowercase. # 1. The scheme and host MUST be in lowercase.
scheme = scheme.lower() scheme = scheme.lower()
netloc = netloc.lower()
# Note: if ``host`` is used, it will be converted to lowercase below # Note: if ``host`` is used, it will be converted to lowercase below
if hostname is not None:
hostname = hostname.lower()
# 2. The host and port values MUST match the content of the HTTP # 2. The host and port values MUST match the content of the HTTP
# request "Host" header field. # request "Host" header field.
if host is not None: if host is not None:
netloc = host.lower() # override value in uri with provided host # NOTE: override value in uri with provided host
# Host argument is equal to netloc. It means it's missing scheme.
# Add it back, before parsing.
host = host.lower()
host = f"{scheme}://{host}"
output = urlparse.urlparse(host)
hostname = output.hostname
port = output.port
# 3. The port MUST be included if it is not the default port for the # 3. The port MUST be included if it is not the default port for the
# scheme, and MUST be excluded if it is the default. Specifically, # scheme, and MUST be excluded if it is the default. Specifically,
@ -170,33 +184,28 @@ def base_string_uri(uri: str, host: str = None) -> str:
# .. _`RFC2616`: https://tools.ietf.org/html/rfc2616 # .. _`RFC2616`: https://tools.ietf.org/html/rfc2616
# .. _`RFC2818`: https://tools.ietf.org/html/rfc2818 # .. _`RFC2818`: https://tools.ietf.org/html/rfc2818
if ':' in netloc: if hostname is None:
# Contains a colon ":", so try to parse as "host:port" raise ValueError('missing host')
hostname, port_str = netloc.split(':', 1) # NOTE: Try guessing if we're dealing with IP or hostname
try:
hostname = ipaddress.ip_address(hostname)
except ValueError:
pass
if len(hostname) == 0: if isinstance(hostname, ipaddress.IPv6Address):
raise ValueError('missing host') # error: netloc was ":port" or ":" hostname = f"[{hostname}]"
elif isinstance(hostname, ipaddress.IPv4Address):
hostname = f"{hostname}"
if len(port_str) == 0: if port is not None and not (0 < port <= 65535):
netloc = hostname # was "host:", so just use the host part raise ValueError('port out of range') # 16-bit unsigned ints
else: if (scheme, port) in (('http', 80), ('https', 443)):
try: netloc = hostname # default port for scheme: exclude port num
port_num = int(port_str) # try to parse into an integer number elif port:
except ValueError: netloc = f"{hostname}:{port}" # use hostname:port
raise ValueError('port is not an integer')
if port_num <= 0 or 65535 < port_num:
raise ValueError('port out of range') # 16-bit unsigned ints
if (scheme, port_num) in (('http', 80), ('https', 443)):
netloc = hostname # default port for scheme: exclude port num
else:
netloc = hostname + ':' + str(port_num) # use hostname:port
else: else:
# Does not contain a colon, so entire value must be the hostname netloc = hostname
if len(netloc) == 0:
raise ValueError('missing host') # error: netloc was empty string
v = urlparse.urlunparse((scheme, netloc, path, params, '', '')) v = urlparse.urlunparse((scheme, netloc, path, params, '', ''))

View file

@ -39,7 +39,7 @@ class BackendApplicationClient(Client):
format per `Appendix B`_ in the HTTP request entity-body: format per `Appendix B`_ in the HTTP request entity-body:
:param body: Existing request body (URL encoded string) to embed parameters :param body: Existing request body (URL encoded string) to embed parameters
into. This may contain extra paramters. Default ''. into. This may contain extra parameters. Default ''.
:param scope: The scope of the access request as described by :param scope: The scope of the access request as described by
`Section 3.3`_. `Section 3.3`_.

View file

@ -6,12 +6,12 @@ oauthlib.oauth2.rfc6749
This module is an implementation of various logic needed This module is an implementation of various logic needed
for consuming OAuth 2.0 RFC6749. for consuming OAuth 2.0 RFC6749.
""" """
import base64
import hashlib
import re
import secrets
import time import time
import warnings import warnings
import secrets
import re
import hashlib
import base64
from oauthlib.common import generate_token from oauthlib.common import generate_token
from oauthlib.oauth2.rfc6749 import tokens from oauthlib.oauth2.rfc6749 import tokens
@ -228,26 +228,21 @@ class Client:
required parameters to the authorization URL. required parameters to the authorization URL.
:param authorization_url: Provider authorization endpoint URL. :param authorization_url: Provider authorization endpoint URL.
:param state: CSRF protection string. Will be automatically created if :param state: CSRF protection string. Will be automatically created if
not provided. The generated state is available via the ``state`` not provided. The generated state is available via the ``state``
attribute. Clients should verify that the state is unchanged and attribute. Clients should verify that the state is unchanged and
present in the authorization response. This verification is done present in the authorization response. This verification is done
automatically if using the ``authorization_response`` parameter automatically if using the ``authorization_response`` parameter
with ``prepare_token_request``. with ``prepare_token_request``.
:param redirect_url: Redirect URL to which the user will be returned :param redirect_url: Redirect URL to which the user will be returned
after authorization. Must be provided unless previously setup with after authorization. Must be provided unless previously setup with
the provider. If provided then it must also be provided in the the provider. If provided then it must also be provided in the
token request. token request.
:param scope: List of scopes to request. Must be equal to :param scope: List of scopes to request. Must be equal to
or a subset of the scopes granted when obtaining the refresh or a subset of the scopes granted when obtaining the refresh
token. If none is provided, the ones provided in the constructor are token. If none is provided, the ones provided in the constructor are
used. used.
:param kwargs: Additional parameters to included in the request. :param kwargs: Additional parameters to included in the request.
:returns: The prepared request tuple with (url, headers, body). :returns: The prepared request tuple with (url, headers, body).
""" """
if not is_secure_transport(authorization_url): if not is_secure_transport(authorization_url):
@ -271,22 +266,16 @@ class Client:
credentials. credentials.
:param token_url: Provider token creation endpoint URL. :param token_url: Provider token creation endpoint URL.
:param authorization_response: The full redirection URL string, i.e. :param authorization_response: The full redirection URL string, i.e.
the location to which the user was redirected after successfull the location to which the user was redirected after successful
authorization. Used to mine credentials needed to obtain a token authorization. Used to mine credentials needed to obtain a token
in this step, such as authorization code. in this step, such as authorization code.
:param redirect_url: The redirect_url supplied with the authorization :param redirect_url: The redirect_url supplied with the authorization
request (if there was one). request (if there was one).
:param state: :param state:
:param body: Existing request body (URL encoded string) to embed parameters :param body: Existing request body (URL encoded string) to embed parameters
into. This may contain extra paramters. Default ''. into. This may contain extra parameters. Default ''.
:param kwargs: Additional parameters to included in the request. :param kwargs: Additional parameters to included in the request.
:returns: The prepared request tuple with (url, headers, body). :returns: The prepared request tuple with (url, headers, body).
""" """
if not is_secure_transport(token_url): if not is_secure_transport(token_url):
@ -312,19 +301,14 @@ class Client:
obtain a new access token, and possibly a new refresh token. obtain a new access token, and possibly a new refresh token.
:param token_url: Provider token refresh endpoint URL. :param token_url: Provider token refresh endpoint URL.
:param refresh_token: Refresh token string. :param refresh_token: Refresh token string.
:param body: Existing request body (URL encoded string) to embed parameters :param body: Existing request body (URL encoded string) to embed parameters
into. This may contain extra paramters. Default ''. into. This may contain extra parameters. Default ''.
:param scope: List of scopes to request. Must be equal to :param scope: List of scopes to request. Must be equal to
or a subset of the scopes granted when obtaining the refresh or a subset of the scopes granted when obtaining the refresh
token. If none is provided, the ones provided in the constructor are token. If none is provided, the ones provided in the constructor are
used. used.
:param kwargs: Additional parameters to included in the request. :param kwargs: Additional parameters to included in the request.
:returns: The prepared request tuple with (url, headers, body). :returns: The prepared request tuple with (url, headers, body).
""" """
if not is_secure_transport(token_url): if not is_secure_transport(token_url):
@ -341,20 +325,14 @@ class Client:
"""Prepare a token revocation request. """Prepare a token revocation request.
:param revocation_url: Provider token revocation endpoint URL. :param revocation_url: Provider token revocation endpoint URL.
:param token: The access or refresh token to be revoked (string). :param token: The access or refresh token to be revoked (string).
:param token_type_hint: ``"access_token"`` (default) or :param token_type_hint: ``"access_token"`` (default) or
``"refresh_token"``. This is optional and if you wish to not pass it you ``"refresh_token"``. This is optional and if you wish to not pass it you
must provide ``token_type_hint=None``. must provide ``token_type_hint=None``.
:param body: :param body:
:param callback: A jsonp callback such as ``package.callback`` to be invoked :param callback: A jsonp callback such as ``package.callback`` to be invoked
upon receiving the response. Not that it should not include a () suffix. upon receiving the response. Not that it should not include a () suffix.
:param kwargs: Additional parameters to included in the request. :param kwargs: Additional parameters to included in the request.
:returns: The prepared request tuple with (url, headers, body). :returns: The prepared request tuple with (url, headers, body).
Note that JSONP request may use GET requests as the parameters will Note that JSONP request may use GET requests as the parameters will
@ -362,7 +340,7 @@ class Client:
An example of a revocation request An example of a revocation request
.. code-block: http .. code-block:: http
POST /revoke HTTP/1.1 POST /revoke HTTP/1.1
Host: server.example.com Host: server.example.com
@ -373,7 +351,7 @@ class Client:
An example of a jsonp revocation request An example of a jsonp revocation request
.. code-block: http .. code-block:: http
GET /revoke?token=agabcdefddddafdd&callback=package.myCallback HTTP/1.1 GET /revoke?token=agabcdefddddafdd&callback=package.myCallback HTTP/1.1
Host: server.example.com Host: server.example.com
@ -382,9 +360,9 @@ class Client:
and an error response and an error response
.. code-block: http .. code-block:: javascript
package.myCallback({"error":"unsupported_token_type"}); package.myCallback({"error":"unsupported_token_type"});
Note that these requests usually require client credentials, client_id in Note that these requests usually require client credentials, client_id in
the case for public clients and provider specific authentication the case for public clients and provider specific authentication
@ -408,9 +386,10 @@ class Client:
:param body: The response body from the token request. :param body: The response body from the token request.
:param scope: Scopes originally requested. If none is provided, the ones :param scope: Scopes originally requested. If none is provided, the ones
provided in the constructor are used. provided in the constructor are used.
:return: Dictionary of token parameters. :return: Dictionary of token parameters.
:raises: Warning if scope has changed. OAuth2Error if response is invalid. :raises: Warning if scope has changed. :py:class:`oauthlib.oauth2.errors.OAuth2Error`
if response is invalid.
These response are json encoded and could easily be parsed without These response are json encoded and could easily be parsed without
the assistance of OAuthLib. However, there are a few subtle issues the assistance of OAuthLib. However, there are a few subtle issues
@ -436,7 +415,7 @@ class Client:
If omitted, the authorization server SHOULD provide the If omitted, the authorization server SHOULD provide the
expiration time via other means or document the default value. expiration time via other means or document the default value.
**scope** **scope**
Providers may supply this in all responses but are required to only Providers may supply this in all responses but are required to only
if it has changed since the authorization request. if it has changed since the authorization request.
@ -454,20 +433,16 @@ class Client:
If the authorization server issued a refresh token to the client, the If the authorization server issued a refresh token to the client, the
client makes a refresh request to the token endpoint by adding the client makes a refresh request to the token endpoint by adding the
following parameters using the "application/x-www-form-urlencoded" following parameters using the `application/x-www-form-urlencoded`
format in the HTTP request entity-body: format in the HTTP request entity-body:
grant_type :param refresh_token: REQUIRED. The refresh token issued to the client.
REQUIRED. Value MUST be set to "refresh_token". :param scope: OPTIONAL. The scope of the access request as described by
refresh_token Section 3.3. The requested scope MUST NOT include any scope
REQUIRED. The refresh token issued to the client. not originally granted by the resource owner, and if omitted is
scope treated as equal to the scope originally granted by the
OPTIONAL. The scope of the access request as described by resource owner. Note that if none is provided, the ones provided
Section 3.3. The requested scope MUST NOT include any scope in the constructor are used if any.
not originally granted by the resource owner, and if omitted is
treated as equal to the scope originally granted by the
resource owner. Note that if none is provided, the ones provided
in the constructor are used if any.
""" """
refresh_token = refresh_token or self.refresh_token refresh_token = refresh_token or self.refresh_token
scope = self.scope if scope is None else scope scope = self.scope if scope is None else scope
@ -492,18 +467,21 @@ class Client:
def create_code_verifier(self, length): def create_code_verifier(self, length):
"""Create PKCE **code_verifier** used in computing **code_challenge**. """Create PKCE **code_verifier** used in computing **code_challenge**.
See `RFC7636 Section 4.1`_
:param length: REQUIRED. The length of the code_verifier. :param length: REQUIRED. The length of the code_verifier.
The client first creates a code verifier, "code_verifier", for each The client first creates a code verifier, "code_verifier", for each
OAuth 2.0 [RFC6749] Authorization Request, in the following manner: OAuth 2.0 [RFC6749] Authorization Request, in the following manner:
code_verifier = high-entropy cryptographic random STRING using the .. code-block:: text
unreserved characters [A-Z] / [a-z] / [0-9] / "-" / "." / "_" / "~"
from Section 2.3 of [RFC3986], with a minimum length of 43 characters code_verifier = high-entropy cryptographic random STRING using the
and a maximum length of 128 characters. unreserved characters [A-Z] / [a-z] / [0-9] / "-" / "." / "_" / "~"
from Section 2.3 of [RFC3986], with a minimum length of 43 characters
.. _`Section 4.1`: https://tools.ietf.org/html/rfc7636#section-4.1 and a maximum length of 128 characters.
.. _`RFC7636 Section 4.1`: https://tools.ietf.org/html/rfc7636#section-4.1
""" """
code_verifier = None code_verifier = None
@ -525,33 +503,30 @@ class Client:
def create_code_challenge(self, code_verifier, code_challenge_method=None): def create_code_challenge(self, code_verifier, code_challenge_method=None):
"""Create PKCE **code_challenge** derived from the **code_verifier**. """Create PKCE **code_challenge** derived from the **code_verifier**.
See `RFC7636 Section 4.2`_
:param code_verifier: REQUIRED. The **code_verifier** generated from create_code_verifier(). :param code_verifier: REQUIRED. The **code_verifier** generated from `create_code_verifier()`.
:param code_challenge_method: OPTIONAL. The method used to derive the **code_challenge**. Acceptable :param code_challenge_method: OPTIONAL. The method used to derive the **code_challenge**. Acceptable values include `S256`. DEFAULT is `plain`.
values include "S256". DEFAULT is "plain".
The client then creates a code challenge derived from the code
The client then creates a code challenge derived from the code
verifier by using one of the following transformations on the code verifier by using one of the following transformations on the code
verifier: verifier::
plain plain
code_challenge = code_verifier code_challenge = code_verifier
S256
code_challenge = BASE64URL-ENCODE(SHA256(ASCII(code_verifier)))
S256 If the client is capable of using `S256`, it MUST use `S256`, as
code_challenge = BASE64URL-ENCODE(SHA256(ASCII(code_verifier))) `S256` is Mandatory To Implement (MTI) on the server. Clients are
permitted to use `plain` only if they cannot support `S256` for some
If the client is capable of using "S256", it MUST use "S256", as
"S256" is Mandatory To Implement (MTI) on the server. Clients are
permitted to use "plain" only if they cannot support "S256" for some
technical reason and know via out-of-band configuration that the technical reason and know via out-of-band configuration that the
server supports "plain". server supports `plain`.
The plain transformation is for compatibility with existing The plain transformation is for compatibility with existing
deployments and for constrained environments that can't use the S256 deployments and for constrained environments that can't use the S256 transformation.
transformation.
.. _`Section 4.2`: https://tools.ietf.org/html/rfc7636#section-4.2 .. _`RFC7636 Section 4.2`: https://tools.ietf.org/html/rfc7636#section-4.2
""" """
code_challenge = None code_challenge = None

View file

@ -49,7 +49,7 @@ class LegacyApplicationClient(Client):
:param username: The resource owner username. :param username: The resource owner username.
:param password: The resource owner password. :param password: The resource owner password.
:param body: Existing request body (URL encoded string) to embed parameters :param body: Existing request body (URL encoded string) to embed parameters
into. This may contain extra paramters. Default ''. into. This may contain extra parameters. Default ''.
:param scope: The scope of the access request as described by :param scope: The scope of the access request as described by
`Section 3.3`_. `Section 3.3`_.
:param include_client_id: `True` to send the `client_id` in the :param include_client_id: `True` to send the `client_id` in the

View file

@ -55,7 +55,7 @@ class MobileApplicationClient(Client):
using the "application/x-www-form-urlencoded" format, per `Appendix B`_: using the "application/x-www-form-urlencoded" format, per `Appendix B`_:
:param redirect_uri: OPTIONAL. The redirect URI must be an absolute URI :param redirect_uri: OPTIONAL. The redirect URI must be an absolute URI
and it should have been registerd with the OAuth and it should have been registered with the OAuth
provider prior to use. As described in `Section 3.1.2`_. provider prior to use. As described in `Section 3.1.2`_.
:param scope: OPTIONAL. The scope of the access request as described by :param scope: OPTIONAL. The scope of the access request as described by

View file

@ -31,7 +31,7 @@ class ServiceApplicationClient(Client):
def __init__(self, client_id, private_key=None, subject=None, issuer=None, def __init__(self, client_id, private_key=None, subject=None, issuer=None,
audience=None, **kwargs): audience=None, **kwargs):
"""Initalize a JWT client with defaults for implicit use later. """Initialize a JWT client with defaults for implicit use later.
:param client_id: Client identifier given by the OAuth provider upon :param client_id: Client identifier given by the OAuth provider upon
registration. registration.
@ -99,7 +99,7 @@ class ServiceApplicationClient(Client):
:param extra_claims: A dict of additional claims to include in the JWT. :param extra_claims: A dict of additional claims to include in the JWT.
:param body: Existing request body (URL encoded string) to embed parameters :param body: Existing request body (URL encoded string) to embed parameters
into. This may contain extra paramters. Default ''. into. This may contain extra parameters. Default ''.
:param scope: The scope of the access request. :param scope: The scope of the access request.

View file

@ -49,7 +49,7 @@ class WebApplicationClient(Client):
using the "application/x-www-form-urlencoded" format, per `Appendix B`_: using the "application/x-www-form-urlencoded" format, per `Appendix B`_:
:param redirect_uri: OPTIONAL. The redirect URI must be an absolute URI :param redirect_uri: OPTIONAL. The redirect URI must be an absolute URI
and it should have been registerd with the OAuth and it should have been registered with the OAuth
provider prior to use. As described in `Section 3.1.2`_. provider prior to use. As described in `Section 3.1.2`_.
:param scope: OPTIONAL. The scope of the access request as described by :param scope: OPTIONAL. The scope of the access request as described by
@ -117,7 +117,7 @@ class WebApplicationClient(Client):
values MUST be identical. values MUST be identical.
:param body: Existing request body (URL encoded string) to embed parameters :param body: Existing request body (URL encoded string) to embed parameters
into. This may contain extra paramters. Default ''. into. This may contain extra parameters. Default ''.
:param include_client_id: `True` (default) to send the `client_id` in the :param include_client_id: `True` (default) to send the `client_id` in the
body of the upstream request. This is required body of the upstream request. This is required

View file

@ -86,9 +86,9 @@ class IntrospectEndpoint(BaseEndpoint):
an HTTP POST request with parameters sent as an HTTP POST request with parameters sent as
"application/x-www-form-urlencoded". "application/x-www-form-urlencoded".
token REQUIRED. The string value of the token. * token REQUIRED. The string value of the token.
* token_type_hint OPTIONAL.
token_type_hint OPTIONAL.
A hint about the type of the token submitted for A hint about the type of the token submitted for
introspection. The protected resource MAY pass this parameter to introspection. The protected resource MAY pass this parameter to
help the authorization server optimize the token lookup. If the help the authorization server optimize the token lookup. If the
@ -96,11 +96,9 @@ class IntrospectEndpoint(BaseEndpoint):
extend its search across all of its supported token types. An extend its search across all of its supported token types. An
authorization server MAY ignore this parameter, particularly if it authorization server MAY ignore this parameter, particularly if it
is able to detect the token type automatically. is able to detect the token type automatically.
* access_token: An Access Token as defined in [`RFC6749`],
`section 1.4`_
* refresh_token: A Refresh Token as defined in [`RFC6749`], * access_token: An Access Token as defined in [`RFC6749`], `section 1.4`_
`section 1.5`_ * refresh_token: A Refresh Token as defined in [`RFC6749`], `section 1.5`_
The introspection endpoint MAY accept other OPTIONAL The introspection endpoint MAY accept other OPTIONAL
parameters to provide further context to the query. For parameters to provide further context to the query. For

View file

@ -10,7 +10,7 @@ import copy
import json import json
import logging import logging
from .. import grant_types from .. import grant_types, utils
from .authorization import AuthorizationEndpoint from .authorization import AuthorizationEndpoint
from .base import BaseEndpoint, catch_errors_and_unavailability from .base import BaseEndpoint, catch_errors_and_unavailability
from .introspect import IntrospectEndpoint from .introspect import IntrospectEndpoint
@ -68,7 +68,7 @@ class MetadataEndpoint(BaseEndpoint):
raise ValueError("key {} is a mandatory metadata.".format(key)) raise ValueError("key {} is a mandatory metadata.".format(key))
elif is_issuer: elif is_issuer:
if not array[key].startswith("https"): if not utils.is_secure_transport(array[key]):
raise ValueError("key {}: {} must be an HTTPS URL".format(key, array[key])) raise ValueError("key {}: {} must be an HTTPS URL".format(key, array[key]))
if "?" in array[key] or "&" in array[key] or "#" in array[key]: if "?" in array[key] or "&" in array[key] or "#" in array[key]:
raise ValueError("key {}: {} must not contain query or fragment components".format(key, array[key])) raise ValueError("key {}: {} must not contain query or fragment components".format(key, array[key]))

View file

@ -42,7 +42,7 @@ class RevocationEndpoint(BaseEndpoint):
The authorization server responds with HTTP status code 200 if the The authorization server responds with HTTP status code 200 if the
token has been revoked sucessfully or if the client submitted an token has been revoked successfully or if the client submitted an
invalid token. invalid token.
Note: invalid tokens do not cause an error response since the client Note: invalid tokens do not cause an error response since the client
@ -95,7 +95,7 @@ class RevocationEndpoint(BaseEndpoint):
submitted for revocation. Clients MAY pass this parameter in order to submitted for revocation. Clients MAY pass this parameter in order to
help the authorization server to optimize the token lookup. If the help the authorization server to optimize the token lookup. If the
server is unable to locate the token using the given hint, it MUST server is unable to locate the token using the given hint, it MUST
extend its search accross all of its supported token types. An extend its search across all of its supported token types. An
authorization server MAY ignore this parameter, particularly if it is authorization server MAY ignore this parameter, particularly if it is
able to detect the token type automatically. This specification able to detect the token type automatically. This specification
defines two such values: defines two such values:

View file

@ -10,7 +10,6 @@ import logging
from oauthlib import common from oauthlib import common
from .. import errors from .. import errors
from ..utils import is_secure_transport
from .base import GrantTypeBase from .base import GrantTypeBase
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
@ -547,20 +546,3 @@ class AuthorizationCodeGrant(GrantTypeBase):
if challenge_method in self._code_challenge_methods: if challenge_method in self._code_challenge_methods:
return self._code_challenge_methods[challenge_method](verifier, challenge) return self._code_challenge_methods[challenge_method](verifier, challenge)
raise NotImplementedError('Unknown challenge_method %s' % challenge_method) raise NotImplementedError('Unknown challenge_method %s' % challenge_method)
def _create_cors_headers(self, request):
"""If CORS is allowed, create the appropriate headers."""
if 'origin' not in request.headers:
return {}
origin = request.headers['origin']
if not is_secure_transport(origin):
log.debug('Origin "%s" is not HTTPS, CORS not allowed.', origin)
return {}
elif not self.request_validator.is_origin_allowed(
request.client_id, origin, request):
log.debug('Invalid origin "%s", CORS not allowed.', origin)
return {}
else:
log.debug('Valid origin "%s", injecting CORS headers.', origin)
return {'Access-Control-Allow-Origin': origin}

View file

@ -10,6 +10,7 @@ from oauthlib.oauth2.rfc6749 import errors, utils
from oauthlib.uri_validate import is_absolute_uri from oauthlib.uri_validate import is_absolute_uri
from ..request_validator import RequestValidator from ..request_validator import RequestValidator
from ..utils import is_secure_transport
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
@ -248,3 +249,20 @@ class GrantTypeBase:
raise errors.MissingRedirectURIError(request=request) raise errors.MissingRedirectURIError(request=request)
if not is_absolute_uri(request.redirect_uri): if not is_absolute_uri(request.redirect_uri):
raise errors.InvalidRedirectURIError(request=request) raise errors.InvalidRedirectURIError(request=request)
def _create_cors_headers(self, request):
"""If CORS is allowed, create the appropriate headers."""
if 'origin' not in request.headers:
return {}
origin = request.headers['origin']
if not is_secure_transport(origin):
log.debug('Origin "%s" is not HTTPS, CORS not allowed.', origin)
return {}
elif not self.request_validator.is_origin_allowed(
request.client_id, origin, request):
log.debug('Invalid origin "%s", CORS not allowed.', origin)
return {}
else:
log.debug('Valid origin "%s", injecting CORS headers.', origin)
return {'Access-Control-Allow-Origin': origin}

View file

@ -69,6 +69,7 @@ class RefreshTokenGrant(GrantTypeBase):
log.debug('Issuing new token to client id %r (%r), %r.', log.debug('Issuing new token to client id %r (%r), %r.',
request.client_id, request.client, token) request.client_id, request.client, token)
headers.update(self._create_cors_headers(request))
return headers, json.dumps(token), 200 return headers, json.dumps(token), 200
def validate_token_request(self, request): def validate_token_request(self, request):

View file

@ -45,7 +45,7 @@ def prepare_grant_uri(uri, client_id, response_type, redirect_uri=None,
back to the client. The parameter SHOULD be used for back to the client. The parameter SHOULD be used for
preventing cross-site request forgery as described in preventing cross-site request forgery as described in
`Section 10.12`_. `Section 10.12`_.
:param code_challenge: PKCE paramater. A challenge derived from the :param code_challenge: PKCE parameter. A challenge derived from the
code_verifier that is sent in the authorization code_verifier that is sent in the authorization
request, to be verified against later. request, to be verified against later.
:param code_challenge_method: PKCE parameter. A method that was used to derive the :param code_challenge_method: PKCE parameter. A method that was used to derive the

View file

@ -191,6 +191,7 @@ class RequestValidator:
claims associated, or `None` in case the token is unknown. claims associated, or `None` in case the token is unknown.
Below the list of registered claims you should be interested in: Below the list of registered claims you should be interested in:
- scope : space-separated list of scopes - scope : space-separated list of scopes
- client_id : client identifier - client_id : client identifier
- username : human-readable identifier for the resource owner - username : human-readable identifier for the resource owner
@ -204,10 +205,10 @@ class RequestValidator:
- jti : string identifier for the token - jti : string identifier for the token
Note that most of them are coming directly from JWT RFC. More details Note that most of them are coming directly from JWT RFC. More details
can be found in `Introspect Claims`_ or `_JWT Claims`_. can be found in `Introspect Claims`_ or `JWT Claims`_.
The implementation can use *token_type_hint* to improve lookup The implementation can use *token_type_hint* to improve lookup
efficency, but must fallback to other types to be compliant with RFC. efficiency, but must fallback to other types to be compliant with RFC.
The dict of claims is added to request.token after this method. The dict of claims is added to request.token after this method.
@ -443,6 +444,7 @@ class RequestValidator:
- request.user - request.user
- request.scopes - request.scopes
- request.claims (if given) - request.claims (if given)
OBS! The request.user attribute should be set to the resource owner OBS! The request.user attribute should be set to the resource owner
associated with this authorization code. Similarly request.scopes associated with this authorization code. Similarly request.scopes
must also be set. must also be set.
@ -451,6 +453,7 @@ class RequestValidator:
If PKCE is enabled (see 'is_pkce_required' and 'save_authorization_code') If PKCE is enabled (see 'is_pkce_required' and 'save_authorization_code')
you MUST set the following based on the information stored: you MUST set the following based on the information stored:
- request.code_challenge - request.code_challenge
- request.code_challenge_method - request.code_challenge_method
@ -561,7 +564,7 @@ class RequestValidator:
OBS! The validation should also set the user attribute of the request OBS! The validation should also set the user attribute of the request
to a valid resource owner, i.e. request.user = username or similar. If to a valid resource owner, i.e. request.user = username or similar. If
not set you will be unable to associate a token with a user in the not set you will be unable to associate a token with a user in the
persistance method used (commonly, save_bearer_token). persistence method used (commonly, save_bearer_token).
:param username: Unicode username. :param username: Unicode username.
:param password: Unicode password. :param password: Unicode password.
@ -671,6 +674,7 @@ class RequestValidator:
Method is used by: Method is used by:
- Authorization Code Grant - Authorization Code Grant
- Refresh Token Grant
""" """
return False return False

View file

@ -257,6 +257,7 @@ def get_token_from_header(request):
class TokenBase: class TokenBase:
__slots__ = ()
def __call__(self, request, refresh_token=False): def __call__(self, request, refresh_token=False):
raise NotImplementedError('Subclasses must implement this method.') raise NotImplementedError('Subclasses must implement this method.')

View file

@ -5,12 +5,11 @@ oauthlib.oauth2.rfc8628
This module is an implementation of various logic needed This module is an implementation of various logic needed
for consuming and providing OAuth 2.0 Device Authorization RFC8628. for consuming and providing OAuth 2.0 Device Authorization RFC8628.
""" """
from oauthlib.common import add_params_to_uri
from oauthlib.oauth2 import BackendApplicationClient, Client from oauthlib.oauth2 import BackendApplicationClient, Client
from oauthlib.oauth2.rfc6749.errors import InsecureTransportError from oauthlib.oauth2.rfc6749.errors import InsecureTransportError
from oauthlib.oauth2.rfc6749.parameters import prepare_token_request from oauthlib.oauth2.rfc6749.parameters import prepare_token_request
from oauthlib.oauth2.rfc6749.utils import is_secure_transport, list_to_scope from oauthlib.oauth2.rfc6749.utils import is_secure_transport, list_to_scope
from oauthlib.common import add_params_to_uri
class DeviceClient(Client): class DeviceClient(Client):
@ -62,7 +61,7 @@ class DeviceClient(Client):
body. body.
:param body: Existing request body (URL encoded string) to embed parameters :param body: Existing request body (URL encoded string) to embed parameters
into. This may contain extra paramters. Default ''. into. This may contain extra parameters. Default ''.
:param scope: The scope of the access request as described by :param scope: The scope of the access request as described by
`Section 3.3`_. `Section 3.3`_.
@ -84,6 +83,8 @@ class DeviceClient(Client):
>>> client.prepare_request_body(scope=['hello', 'world']) >>> client.prepare_request_body(scope=['hello', 'world'])
'grant_type=urn:ietf:params:oauth:grant-type:device_code&scope=hello+world' 'grant_type=urn:ietf:params:oauth:grant-type:device_code&scope=hello+world'
.. _`Section 3.2.1`: https://datatracker.ietf.org/doc/html/rfc6749#section-3.2.1
.. _`Section 3.3`: https://datatracker.ietf.org/doc/html/rfc6749#section-3.3
.. _`Section 3.4`: https://datatracker.ietf.org/doc/html/rfc8628#section-3.4 .. _`Section 3.4`: https://datatracker.ietf.org/doc/html/rfc8628#section-3.4
""" """

View file

@ -69,7 +69,7 @@ class UserInfoEndpoint(BaseEndpoint):
5.3.1. UserInfo Request 5.3.1. UserInfo Request
The Client sends the UserInfo Request using either HTTP GET or HTTP The Client sends the UserInfo Request using either HTTP GET or HTTP
POST. The Access Token obtained from an OpenID Connect Authentication POST. The Access Token obtained from an OpenID Connect Authentication
Request MUST be sent as a Bearer Token, per Section 2 of OAuth 2.0 Request MUST be sent as a Bearer Token, per `Section 2`_ of OAuth 2.0
Bearer Token Usage [RFC6750]. Bearer Token Usage [RFC6750].
It is RECOMMENDED that the request use the HTTP GET method and the It is RECOMMENDED that the request use the HTTP GET method and the
@ -77,21 +77,28 @@ class UserInfoEndpoint(BaseEndpoint):
The following is a non-normative example of a UserInfo Request: The following is a non-normative example of a UserInfo Request:
GET /userinfo HTTP/1.1 .. code-block:: http
Host: server.example.com
Authorization: Bearer SlAV32hkKG GET /userinfo HTTP/1.1
Host: server.example.com
Authorization: Bearer SlAV32hkKG
5.3.3. UserInfo Error Response 5.3.3. UserInfo Error Response
When an error condition occurs, the UserInfo Endpoint returns an Error When an error condition occurs, the UserInfo Endpoint returns an Error
Response as defined in Section 3 of OAuth 2.0 Bearer Token Usage Response as defined in `Section 3`_ of OAuth 2.0 Bearer Token Usage
[RFC6750]. (HTTP errors unrelated to RFC 6750 are returned to the User [RFC6750]. (HTTP errors unrelated to RFC 6750 are returned to the User
Agent using the appropriate HTTP status code.) Agent using the appropriate HTTP status code.)
The following is a non-normative example of a UserInfo Error Response: The following is a non-normative example of a UserInfo Error Response:
HTTP/1.1 401 Unauthorized .. code-block:: http
WWW-Authenticate: Bearer error="invalid_token",
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer error="invalid_token",
error_description="The Access Token expired" error_description="The Access Token expired"
.. _`Section 2`: https://datatracker.ietf.org/doc/html/rfc6750#section-2
.. _`Section 3`: https://datatracker.ietf.org/doc/html/rfc6750#section-3
""" """
if not self.bearer.validate_request(request): if not self.bearer.validate_request(request):
raise errors.InvalidTokenError() raise errors.InvalidTokenError()

View file

@ -8,7 +8,6 @@ from oauthlib.oauth2.rfc6749.errors import (
ConsentRequired, InvalidRequestError, LoginRequired, ConsentRequired, InvalidRequestError, LoginRequired,
) )
log = logging.getLogger(__name__) log = logging.getLogger(__name__)

View file

@ -84,7 +84,7 @@ class AuthorizationTokenGrantDispatcher(Dispatcher):
code = parameters.get('code', None) code = parameters.get('code', None)
redirect_uri = parameters.get('redirect_uri', None) redirect_uri = parameters.get('redirect_uri', None)
# If code is not pressent fallback to `default_grant` which will # If code is not present fallback to `default_grant` which will
# raise an error for the missing `code` in `create_token_response` step. # raise an error for the missing `code` in `create_token_response` step.
if code: if code:
scopes = self.request_validator.get_authorization_code_scopes(client_id, code, redirect_uri, request) scopes = self.request_validator.get_authorization_code_scopes(client_id, code, redirect_uri, request)

View file

@ -4,7 +4,9 @@ authlib.openid.connect.core.tokens
This module contains methods for adding JWT tokens to requests. This module contains methods for adding JWT tokens to requests.
""" """
from oauthlib.oauth2.rfc6749.tokens import TokenBase, random_token_generator, get_token_from_header from oauthlib.oauth2.rfc6749.tokens import (
TokenBase, get_token_from_header, random_token_generator,
)
class JWTToken(TokenBase): class JWTToken(TokenBase):

View file

@ -66,7 +66,7 @@ IPv4address = r"%(dec_octet)s \. %(dec_octet)s \. %(dec_octet)s \. %(dec_octet)s
) )
# IPv6address # IPv6address
IPv6address = r"([A-Fa-f0-9:]+:+)+[A-Fa-f0-9]+" IPv6address = r"([A-Fa-f0-9:]+[:$])[A-Fa-f0-9]{1,4}"
# IPvFuture = "v" 1*HEXDIG "." 1*( unreserved / sub-delims / ":" ) # IPvFuture = "v" 1*HEXDIG "." 1*( unreserved / sub-delims / ":" )
IPvFuture = r"v %(HEXDIG)s+ \. (?: %(unreserved)s | %(sub_delims)s | : )+" % locals() IPvFuture = r"v %(HEXDIG)s+ \. (?: %(unreserved)s | %(sub_delims)s | : )+" % locals()

View file

@ -1,3 +1,4 @@
# ruff: noqa: F401
import logging import logging
from .oauth1_auth import OAuth1 from .oauth1_auth import OAuth1
@ -5,7 +6,7 @@ from .oauth1_session import OAuth1Session
from .oauth2_auth import OAuth2 from .oauth2_auth import OAuth2
from .oauth2_session import OAuth2Session, TokenUpdated from .oauth2_session import OAuth2Session, TokenUpdated
__version__ = "1.3.1" __version__ = "2.0.0"
import requests import requests

View file

@ -1,5 +1,4 @@
from __future__ import absolute_import # ruff: noqa: F401
from .facebook import facebook_compliance_fix from .facebook import facebook_compliance_fix
from .fitbit import fitbit_compliance_fix from .fitbit import fitbit_compliance_fix
from .slack import slack_compliance_fix from .slack import slack_compliance_fix

View file

@ -1,14 +1,12 @@
import json import json
from oauthlib.common import to_unicode
def douban_compliance_fix(session): def douban_compliance_fix(session):
def fix_token_type(r): def fix_token_type(r):
token = json.loads(r.text) token = json.loads(r.text)
token.setdefault("token_type", "Bearer") token.setdefault("token_type", "Bearer")
fixed_token = json.dumps(token) fixed_token = json.dumps(token)
r._content = to_unicode(fixed_token).encode("utf-8") r._content = fixed_token.encode()
return r return r
session._client_default_token_placement = "query" session._client_default_token_placement = "query"

View file

@ -1,5 +1,4 @@
import json import json
from oauthlib.common import to_unicode
def ebay_compliance_fix(session): def ebay_compliance_fix(session):
@ -13,7 +12,7 @@ def ebay_compliance_fix(session):
if token.get("token_type") in ["Application Access Token", "User Access Token"]: if token.get("token_type") in ["Application Access Token", "User Access Token"]:
token["token_type"] = "Bearer" token["token_type"] = "Bearer"
fixed_token = json.dumps(token) fixed_token = json.dumps(token)
response._content = to_unicode(fixed_token).encode("utf-8") response._content = fixed_token.encode()
return response return response

View file

@ -1,11 +1,5 @@
from json import dumps from json import dumps
from urllib.parse import parse_qsl
try:
from urlparse import parse_qsl
except ImportError:
from urllib.parse import parse_qsl
from oauthlib.common import to_unicode
def facebook_compliance_fix(session): def facebook_compliance_fix(session):
@ -26,7 +20,7 @@ def facebook_compliance_fix(session):
if expires is not None: if expires is not None:
token["expires_in"] = expires token["expires_in"] = expires
token["token_type"] = "Bearer" token["token_type"] = "Bearer"
r._content = to_unicode(dumps(token)).encode("UTF-8") r._content = dumps(token).encode()
return r return r
session.register_compliance_hook("access_token_response", _compliance_fix) session.register_compliance_hook("access_token_response", _compliance_fix)

View file

@ -8,8 +8,6 @@ MissingTokenError.
from json import loads, dumps from json import loads, dumps
from oauthlib.common import to_unicode
def fitbit_compliance_fix(session): def fitbit_compliance_fix(session):
def _missing_error(r): def _missing_error(r):
@ -17,7 +15,7 @@ def fitbit_compliance_fix(session):
if "errors" in token: if "errors" in token:
# Set the error to the first one we have # Set the error to the first one we have
token["error"] = token["errors"][0]["errorType"] token["error"] = token["errors"][0]["errorType"]
r._content = to_unicode(dumps(token)).encode("UTF-8") r._content = dumps(token).encode()
return r return r
session.register_compliance_hook("access_token_response", _missing_error) session.register_compliance_hook("access_token_response", _missing_error)

View file

@ -1,7 +1,4 @@
try: from urllib.parse import urlparse, parse_qs
from urlparse import urlparse, parse_qs
except ImportError:
from urllib.parse import urlparse, parse_qs
from oauthlib.common import add_params_to_uri from oauthlib.common import add_params_to_uri

View file

@ -1,21 +1,19 @@
import json import json
from oauthlib.common import to_unicode
def mailchimp_compliance_fix(session): def mailchimp_compliance_fix(session):
def _null_scope(r): def _null_scope(r):
token = json.loads(r.text) token = json.loads(r.text)
if "scope" in token and token["scope"] is None: if "scope" in token and token["scope"] is None:
token.pop("scope") token.pop("scope")
r._content = to_unicode(json.dumps(token)).encode("utf-8") r._content = json.dumps(token).encode()
return r return r
def _non_zero_expiration(r): def _non_zero_expiration(r):
token = json.loads(r.text) token = json.loads(r.text)
if "expires_in" in token and token["expires_in"] == 0: if "expires_in" in token and token["expires_in"] == 0:
token["expires_in"] = 3600 token["expires_in"] = 3600
r._content = to_unicode(json.dumps(token)).encode("utf-8") r._content = json.dumps(token).encode()
return r return r
session.register_compliance_hook("access_token_response", _null_scope) session.register_compliance_hook("access_token_response", _null_scope)

View file

@ -1,8 +1,6 @@
from json import dumps, loads from json import dumps, loads
import re import re
from oauthlib.common import to_unicode
def plentymarkets_compliance_fix(session): def plentymarkets_compliance_fix(session):
def _to_snake_case(n): def _to_snake_case(n):
@ -22,7 +20,7 @@ def plentymarkets_compliance_fix(session):
for k, v in token.items(): for k, v in token.items():
fixed_token[_to_snake_case(k)] = v fixed_token[_to_snake_case(k)] = v
r._content = to_unicode(dumps(fixed_token)).encode("UTF-8") r._content = dumps(fixed_token).encode()
return r return r
session.register_compliance_hook("access_token_response", _compliance_fix) session.register_compliance_hook("access_token_response", _compliance_fix)

View file

@ -1,7 +1,4 @@
try: from urllib.parse import urlparse, parse_qs
from urlparse import urlparse, parse_qs
except ImportError:
from urllib.parse import urlparse, parse_qs
from oauthlib.common import add_params_to_uri from oauthlib.common import add_params_to_uri

View file

@ -1,13 +1,11 @@
from json import loads, dumps from json import loads, dumps
from oauthlib.common import to_unicode
def weibo_compliance_fix(session): def weibo_compliance_fix(session):
def _missing_token_type(r): def _missing_token_type(r):
token = loads(r.text) token = loads(r.text)
token["token_type"] = "Bearer" token["token_type"] = "Bearer"
r._content = to_unicode(dumps(token)).encode("UTF-8") r._content = dumps(token).encode()
return r return r
session._client.default_token_placement = "query" session._client.default_token_placement = "query"

View file

@ -1,20 +1,15 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from __future__ import unicode_literals
import logging import logging
from oauthlib.common import extract_params from oauthlib.common import extract_params
from oauthlib.oauth1 import Client, SIGNATURE_HMAC, SIGNATURE_TYPE_AUTH_HEADER from oauthlib.oauth1 import Client, SIGNATURE_HMAC, SIGNATURE_TYPE_AUTH_HEADER
from oauthlib.oauth1 import SIGNATURE_TYPE_BODY from oauthlib.oauth1 import SIGNATURE_TYPE_BODY
from requests.compat import is_py3
from requests.utils import to_native_string from requests.utils import to_native_string
from requests.auth import AuthBase from requests.auth import AuthBase
CONTENT_TYPE_FORM_URLENCODED = "application/x-www-form-urlencoded" CONTENT_TYPE_FORM_URLENCODED = "application/x-www-form-urlencoded"
CONTENT_TYPE_MULTI_PART = "multipart/form-data" CONTENT_TYPE_MULTI_PART = "multipart/form-data"
if is_py3:
unicode = str
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
@ -83,7 +78,7 @@ class OAuth1(AuthBase):
or self.client.signature_type == SIGNATURE_TYPE_BODY or self.client.signature_type == SIGNATURE_TYPE_BODY
): ):
content_type = CONTENT_TYPE_FORM_URLENCODED content_type = CONTENT_TYPE_FORM_URLENCODED
if not isinstance(content_type, unicode): if not isinstance(content_type, str):
content_type = content_type.decode("utf-8") content_type = content_type.decode("utf-8")
is_form_encoded = CONTENT_TYPE_FORM_URLENCODED in content_type is_form_encoded = CONTENT_TYPE_FORM_URLENCODED in content_type
@ -96,17 +91,17 @@ class OAuth1(AuthBase):
if is_form_encoded: if is_form_encoded:
r.headers["Content-Type"] = CONTENT_TYPE_FORM_URLENCODED r.headers["Content-Type"] = CONTENT_TYPE_FORM_URLENCODED
r.url, headers, r.body = self.client.sign( r.url, headers, r.body = self.client.sign(
unicode(r.url), unicode(r.method), r.body or "", r.headers str(r.url), str(r.method), r.body or "", r.headers
) )
elif self.force_include_body: elif self.force_include_body:
# To allow custom clients to work on non form encoded bodies. # To allow custom clients to work on non form encoded bodies.
r.url, headers, r.body = self.client.sign( r.url, headers, r.body = self.client.sign(
unicode(r.url), unicode(r.method), r.body or "", r.headers str(r.url), str(r.method), r.body or "", r.headers
) )
else: else:
# Omit body data in the signing of non form-encoded requests # Omit body data in the signing of non form-encoded requests
r.url, headers, _ = self.client.sign( r.url, headers, _ = self.client.sign(
unicode(r.url), unicode(r.method), None, r.headers str(r.url), str(r.method), None, r.headers
) )
r.prepare_headers(headers) r.prepare_headers(headers)

View file

@ -1,9 +1,4 @@
from __future__ import unicode_literals from urllib.parse import urlparse
try:
from urlparse import urlparse
except ImportError:
from urllib.parse import urlparse
import logging import logging
@ -85,7 +80,7 @@ class OAuth1Session(requests.Session):
'https://api.twitter.com/oauth/authorize?oauth_token=sdf0o9823sjdfsdf&oauth_callback=https%3A%2F%2F127.0.0.1%2Fcallback' 'https://api.twitter.com/oauth/authorize?oauth_token=sdf0o9823sjdfsdf&oauth_callback=https%3A%2F%2F127.0.0.1%2Fcallback'
>>> >>>
>>> # Third step. Fetch the access token >>> # Third step. Fetch the access token
>>> redirect_response = raw_input('Paste the full redirect URL here.') >>> redirect_response = input('Paste the full redirect URL here.')
>>> oauth_session.parse_authorization_response(redirect_response) >>> oauth_session.parse_authorization_response(redirect_response)
{ {
'oauth_token: 'kjerht2309u', 'oauth_token: 'kjerht2309u',
@ -258,7 +253,7 @@ class OAuth1Session(requests.Session):
return add_params_to_uri(url, kwargs.items()) return add_params_to_uri(url, kwargs.items())
def fetch_request_token(self, url, realm=None, **request_kwargs): def fetch_request_token(self, url, realm=None, **request_kwargs):
r"""Fetch a request token. """Fetch a request token.
This is the first step in the OAuth 1 workflow. A request token is This is the first step in the OAuth 1 workflow. A request token is
obtained by making a signed post request to url. The token is then obtained by making a signed post request to url. The token is then
@ -267,7 +262,7 @@ class OAuth1Session(requests.Session):
:param url: The request token endpoint URL. :param url: The request token endpoint URL.
:param realm: A list of realms to request access to. :param realm: A list of realms to request access to.
:param \*\*request_kwargs: Optional arguments passed to ''post'' :param request_kwargs: Optional arguments passed to ''post''
function in ''requests.Session'' function in ''requests.Session''
:returns: The response in dict format. :returns: The response in dict format.

View file

@ -1,4 +1,3 @@
from __future__ import unicode_literals
from oauthlib.oauth2 import WebApplicationClient, InsecureTransportError from oauthlib.oauth2 import WebApplicationClient, InsecureTransportError
from oauthlib.oauth2 import is_secure_transport from oauthlib.oauth2 import is_secure_transport
from requests.auth import AuthBase from requests.auth import AuthBase

View file

@ -1,5 +1,3 @@
from __future__ import unicode_literals
import logging import logging
from oauthlib.common import generate_token, urldecode from oauthlib.common import generate_token, urldecode
@ -46,6 +44,7 @@ class OAuth2Session(requests.Session):
token=None, token=None,
state=None, state=None,
token_updater=None, token_updater=None,
pkce=None,
**kwargs **kwargs
): ):
"""Construct a new OAuth 2 client session. """Construct a new OAuth 2 client session.
@ -72,18 +71,23 @@ class OAuth2Session(requests.Session):
set a TokenUpdated warning will be raised when a token set a TokenUpdated warning will be raised when a token
has been refreshed. This warning will carry the token has been refreshed. This warning will carry the token
in its token argument. in its token argument.
:param pkce: Set "S256" or "plain" to enable PKCE. Default is disabled.
:param kwargs: Arguments to pass to the Session constructor. :param kwargs: Arguments to pass to the Session constructor.
""" """
super(OAuth2Session, self).__init__(**kwargs) super(OAuth2Session, self).__init__(**kwargs)
self._client = client or WebApplicationClient(client_id, token=token) self._client = client or WebApplicationClient(client_id, token=token)
self.token = token or {} self.token = token or {}
self.scope = scope self._scope = scope
self.redirect_uri = redirect_uri self.redirect_uri = redirect_uri
self.state = state or generate_token self.state = state or generate_token
self._state = state self._state = state
self.auto_refresh_url = auto_refresh_url self.auto_refresh_url = auto_refresh_url
self.auto_refresh_kwargs = auto_refresh_kwargs or {} self.auto_refresh_kwargs = auto_refresh_kwargs or {}
self.token_updater = token_updater self.token_updater = token_updater
self._pkce = pkce
if self._pkce not in ["S256", "plain", None]:
raise AttributeError("Wrong value for {}(.., pkce={})".format(self.__class__, self._pkce))
# Ensure that requests doesn't do any automatic auth. See #278. # Ensure that requests doesn't do any automatic auth. See #278.
# The default behavior can be re-enabled by setting auth to None. # The default behavior can be re-enabled by setting auth to None.
@ -95,8 +99,24 @@ class OAuth2Session(requests.Session):
"access_token_response": set(), "access_token_response": set(),
"refresh_token_response": set(), "refresh_token_response": set(),
"protected_request": set(), "protected_request": set(),
"refresh_token_request": set(),
"access_token_request": set(),
} }
@property
def scope(self):
"""By default the scope from the client is used, except if overridden"""
if self._scope is not None:
return self._scope
elif self._client is not None:
return self._client.scope
else:
return None
@scope.setter
def scope(self, scope):
self._scope = scope
def new_state(self): def new_state(self):
"""Generates a state string to be used in authorizations.""" """Generates a state string to be used in authorizations."""
try: try:
@ -161,6 +181,13 @@ class OAuth2Session(requests.Session):
:return: authorization_url, state :return: authorization_url, state
""" """
state = state or self.new_state() state = state or self.new_state()
if self._pkce:
self._code_verifier = self._client.create_code_verifier(43)
kwargs["code_challenge_method"] = self._pkce
kwargs["code_challenge"] = self._client.create_code_challenge(
code_verifier=self._code_verifier,
code_challenge_method=self._pkce
)
return ( return (
self._client.prepare_request_uri( self._client.prepare_request_uri(
url, url,
@ -185,7 +212,7 @@ class OAuth2Session(requests.Session):
force_querystring=False, force_querystring=False,
timeout=None, timeout=None,
headers=None, headers=None,
verify=True, verify=None,
proxies=None, proxies=None,
include_client_id=None, include_client_id=None,
client_secret=None, client_secret=None,
@ -252,6 +279,13 @@ class OAuth2Session(requests.Session):
"Please supply either code or " "authorization_response parameters." "Please supply either code or " "authorization_response parameters."
) )
if self._pkce:
if self._code_verifier is None:
raise ValueError(
"Code verifier is not found, authorization URL must be generated before"
)
kwargs["code_verifier"] = self._code_verifier
# Earlier versions of this library build an HTTPBasicAuth header out of # Earlier versions of this library build an HTTPBasicAuth header out of
# `username` and `password`. The RFC states, however these attributes # `username` and `password`. The RFC states, however these attributes
# must be in the request body and not the header. # must be in the request body and not the header.
@ -325,7 +359,7 @@ class OAuth2Session(requests.Session):
headers = headers or { headers = headers or {
"Accept": "application/json", "Accept": "application/json",
"Content-Type": "application/x-www-form-urlencoded;charset=UTF-8", "Content-Type": "application/x-www-form-urlencoded",
} }
self.token = {} self.token = {}
request_kwargs = {} request_kwargs = {}
@ -338,6 +372,12 @@ class OAuth2Session(requests.Session):
else: else:
raise ValueError("The method kwarg must be POST or GET.") raise ValueError("The method kwarg must be POST or GET.")
for hook in self.compliance_hook["access_token_request"]:
log.debug("Invoking access_token_request hook %s.", hook)
token_url, headers, request_kwargs = hook(
token_url, headers, request_kwargs
)
r = self.request( r = self.request(
method=method, method=method,
url=token_url, url=token_url,
@ -388,7 +428,7 @@ class OAuth2Session(requests.Session):
auth=None, auth=None,
timeout=None, timeout=None,
headers=None, headers=None,
verify=True, verify=None,
proxies=None, proxies=None,
**kwargs **kwargs
): ):
@ -426,9 +466,13 @@ class OAuth2Session(requests.Session):
if headers is None: if headers is None:
headers = { headers = {
"Accept": "application/json", "Accept": "application/json",
"Content-Type": ("application/x-www-form-urlencoded;charset=UTF-8"), "Content-Type": ("application/x-www-form-urlencoded"),
} }
for hook in self.compliance_hook["refresh_token_request"]:
log.debug("Invoking refresh_token_request hook %s.", hook)
token_url, headers, body = hook(token_url, headers, body)
r = self.post( r = self.post(
token_url, token_url,
data=dict(urldecode(body)), data=dict(urldecode(body)),
@ -450,7 +494,7 @@ class OAuth2Session(requests.Session):
r = hook(r) r = hook(r)
self.token = self._client.parse_request_body_response(r.text, scope=self.scope) self.token = self._client.parse_request_body_response(r.text, scope=self.scope)
if not "refresh_token" in self.token: if "refresh_token" not in self.token:
log.debug("No new refresh token given. Re-using old.") log.debug("No new refresh token given. Re-using old.")
self.token["refresh_token"] = refresh_token self.token["refresh_token"] = refresh_token
return self.token return self.token
@ -464,6 +508,7 @@ class OAuth2Session(requests.Session):
withhold_token=False, withhold_token=False,
client_id=None, client_id=None,
client_secret=None, client_secret=None,
files=None,
**kwargs **kwargs
): ):
"""Intercept all requests and add the OAuth 2 token if present.""" """Intercept all requests and add the OAuth 2 token if present."""
@ -519,7 +564,7 @@ class OAuth2Session(requests.Session):
log.debug("Supplying headers %s and data %s", headers, data) log.debug("Supplying headers %s and data %s", headers, data)
log.debug("Passing through key word arguments %s.", kwargs) log.debug("Passing through key word arguments %s.", kwargs)
return super(OAuth2Session, self).request( return super(OAuth2Session, self).request(
method, url, headers=headers, data=data, **kwargs method, url, headers=headers, data=data, files=files, **kwargs
) )
def register_compliance_hook(self, hook_type, hook): def register_compliance_hook(self, hook_type, hook):
@ -529,6 +574,8 @@ class OAuth2Session(requests.Session):
access_token_response invoked before token parsing. access_token_response invoked before token parsing.
refresh_token_response invoked before refresh token parsing. refresh_token_response invoked before refresh token parsing.
protected_request invoked before making a request. protected_request invoked before making a request.
access_token_request invoked before making a token fetch request.
refresh_token_request invoked before making a refresh request.
If you find a new hook is needed please send a GitHub PR request If you find a new hook is needed please send a GitHub PR request
or open an issue. or open an issue.

View file

@ -37,7 +37,7 @@ python-dateutil==2.9.0.post0
python-twitter==3.5 python-twitter==3.5
pytz==2024.1 pytz==2024.1
requests==2.31.0 requests==2.31.0
requests-oauthlib==1.3.1 requests-oauthlib==2.0.0
rumps==0.4.0; platform_system == "Darwin" rumps==0.4.0; platform_system == "Darwin"
simplejson==3.19.2 simplejson==3.19.2
six==1.16.0 six==1.16.0