Skip to content

Commit

Permalink
Merge pull request #111 from MaybeNetwork/otp-2021
Browse files Browse the repository at this point in the history
Add two factor callback
  • Loading branch information
bboe committed Jun 5, 2021
2 parents 257cc6b + 535715c commit 437c2e4
Show file tree
Hide file tree
Showing 7 changed files with 261 additions and 3 deletions.
2 changes: 2 additions & 0 deletions CHANGES.rst
Expand Up @@ -10,6 +10,8 @@ Unreleased
**Added**

- Add a ``URITooLarge`` exception.
- :class:`.ScriptAuthorizer` has a new parameter ``two_factor_callback `` that supplies
OTPs (One-Time Passcodes) when :meth:`.ScriptAuthorizer.refresh` is called.
2.0.0 (2021-02-23)
------------------
Expand Down
9 changes: 8 additions & 1 deletion prawcore/auth.py
Expand Up @@ -356,22 +356,29 @@ class ScriptAuthorizer(Authorizer):

AUTHENTICATOR_CLASS = TrustedAuthenticator

def __init__(self, authenticator, username, password):
def __init__(
self, authenticator, username, password, two_factor_callback=None
):
"""Represent a single personal-use authorization to Reddit's API.
:param authenticator: An instance of :class:`TrustedAuthenticator`.
:param username: The Reddit username of one of the application's developers.
:param password: The password associated with ``username``.
:param two_factor_callback: A function that returns OTPs (One-Time
Passcodes), also known as 2FA auth codes. If this function is
provided, prawcore will call it when authenticating.
"""
super(ScriptAuthorizer, self).__init__(authenticator)
self._username = username
self._password = password
self._two_factor_callback = two_factor_callback

def refresh(self):
"""Obtain a new personal-use script type access token."""
self._request_token(
grant_type="password",
username=self._username,
password=self._password,
otp=self._two_factor_callback and self._two_factor_callback(),
)
103 changes: 103 additions & 0 deletions tests/cassettes/ScriptAuthorizer_refresh_with__invalid_otp.json
@@ -0,0 +1,103 @@
{
"http_interactions": [
{
"recorded_at": "2021-05-21T00:48:30",
"request": {
"body": {
"encoding": "utf-8",
"string": "grant_type=password&otp=fake&password=<PASSWORD>&username=<USERNAME>"
},
"headers": {
"Accept": [
"*/*"
],
"Accept-Encoding": [
"gzip, deflate"
],
"Authorization": [
"Basic <BASIC_AUTH>"
],
"Connection": [
"close"
],
"Content-Length": [
"152"
],
"Content-Type": [
"application/x-www-form-urlencoded"
],
"Cookie": [
"edgebucket=7C1qdPEdiInzFy9WaL; loid=bSusfEGk5JXrZYhhyW"
],
"User-Agent": [
"prawcore:test (by /u/bboe) prawcore/2.0.0"
]
},
"method": "POST",
"uri": "https://www.reddit.com/api/v1/access_token"
},
"response": {
"body": {
"encoding": "UTF-8",
"string": "{\"error\": \"invalid_grant\"}"
},
"headers": {
"Accept-Ranges": [
"bytes"
],
"Connection": [
"close"
],
"Content-Length": [
"26"
],
"Content-Type": [
"application/json; charset=UTF-8"
],
"Date": [
"Fri, 21 May 2021 00:48:33 GMT"
],
"Server": [
"snooserv"
],
"Strict-Transport-Security": [
"max-age=15552000; includeSubDomains; preload"
],
"Via": [
"1.1 varnish"
],
"X-Moose": [
"majestic"
],
"cache-control": [
"max-age=0, must-revalidate"
],
"x-content-type-options": [
"nosniff"
],
"x-frame-options": [
"SAMEORIGIN"
],
"x-ratelimit-remaining": [
"297"
],
"x-ratelimit-reset": [
"87"
],
"x-ratelimit-used": [
"3"
],
"x-xss-protection": [
"1; mode=block"
]
},
"status": {
"code": 200,
"message": "OK"
},
"url": "https://www.reddit.com/api/v1/access_token"
}
}
],
"recorded_with": "betamax/0.8.1"
}
103 changes: 103 additions & 0 deletions tests/cassettes/ScriptAuthorizer_refresh_with__valid_otp.json
@@ -0,0 +1,103 @@
{
"http_interactions": [
{
"recorded_at": "2021-05-21T00:43:11",
"request": {
"body": {
"encoding": "utf-8",
"string": "grant_type=password&otp=000000&password=<PASSWORD>&username=<USERNAME>"
},
"headers": {
"Accept": [
"*/*"
],
"Accept-Encoding": [
"gzip, deflate"
],
"Authorization": [
"Basic <BASIC_AUTH>"
],
"Connection": [
"close"
],
"Content-Length": [
"152"
],
"Content-Type": [
"application/x-www-form-urlencoded"
],
"Cookie": [
"edgebucket=W5X0f17fr5uGzS0Jxd; loid=yU0JIU6fMP2ZtL3FsU"
],
"User-Agent": [
"prawcore:test (by /u/bboe) prawcore/2.0.0"
]
},
"method": "POST",
"uri": "https://www.reddit.com/api/v1/access_token"
},
"response": {
"body": {
"encoding": "UTF-8",
"string": "{\"access_token\": \"00000000-000000000000000000000000000000\", \"token_type\": \"bearer\", \"expires_in\": 3600, \"scope\": \"*\"}"
},
"headers": {
"Accept-Ranges": [
"bytes"
],
"Connection": [
"close"
],
"Content-Length": [
"117"
],
"Content-Type": [
"application/json; charset=UTF-8"
],
"Date": [
"Fri, 21 May 2021 00:43:14 GMT"
],
"Server": [
"snooserv"
],
"Strict-Transport-Security": [
"max-age=15552000; includeSubDomains; preload"
],
"Via": [
"1.1 varnish"
],
"X-Moose": [
"majestic"
],
"cache-control": [
"max-age=0, must-revalidate"
],
"x-content-type-options": [
"nosniff"
],
"x-frame-options": [
"SAMEORIGIN"
],
"x-ratelimit-remaining": [
"299"
],
"x-ratelimit-reset": [
"406"
],
"x-ratelimit-used": [
"1"
],
"x-xss-protection": [
"1; mode=block"
]
},
"status": {
"code": 200,
"message": "OK"
},
"url": "https://www.reddit.com/api/v1/access_token"
}
}
],
"recorded_with": "betamax/0.8.1"
}
5 changes: 5 additions & 0 deletions tests/conftest.py
Expand Up @@ -31,6 +31,11 @@ def b64_string(input_string):
return b64encode(input_string.encode("utf-8")).decode("utf-8")


def two_factor_callback():
"""Return an OTP code."""
return None


Betamax.register_request_matcher(JSONBodyMatcher)
Betamax.register_serializer(pretty_json.PrettyJSONSerializer)

Expand Down
37 changes: 36 additions & 1 deletion tests/test_authorizer.py
Expand Up @@ -5,7 +5,7 @@

import prawcore

from .conftest import (
from .conftest import ( # noqa F401
CLIENT_ID,
CLIENT_SECRET,
PASSWORD,
Expand All @@ -14,6 +14,7 @@
REFRESH_TOKEN,
REQUESTOR,
TEMPORARY_GRANT_CODE,
two_factor_callback,
USERNAME,
)

Expand Down Expand Up @@ -340,6 +341,40 @@ def test_refresh(self):
self.assertEqual(set(["*"]), authorizer.scopes)
self.assertTrue(authorizer.is_valid())

def test_refresh_with__valid_otp(self):
authorizer = prawcore.ScriptAuthorizer(
self.authentication,
USERNAME,
PASSWORD,
lambda: "000000",
)
self.assertIsNone(authorizer.access_token)
self.assertIsNone(authorizer.scopes)
self.assertFalse(authorizer.is_valid())

with Betamax(REQUESTOR).use_cassette(
"ScriptAuthorizer_refresh_with__valid_otp"
):
authorizer.refresh()

self.assertIsNotNone(authorizer.access_token)
self.assertEqual(set(["*"]), authorizer.scopes)
self.assertTrue(authorizer.is_valid())

def test_refresh_with__invalid_otp(self):
authorizer = prawcore.ScriptAuthorizer(
self.authentication,
USERNAME,
PASSWORD,
lambda: "fake",
)

with Betamax(REQUESTOR).use_cassette(
"ScriptAuthorizer_refresh_with__invalid_otp"
):
self.assertRaises(prawcore.OAuthException, authorizer.refresh)
self.assertFalse(authorizer.is_valid())

def test_refresh__with_invalid_username_or_password(self):
authorizer = prawcore.ScriptAuthorizer(
self.authentication, USERNAME, "invalidpassword"
Expand Down
5 changes: 4 additions & 1 deletion tests/test_sessions.py
Expand Up @@ -22,6 +22,7 @@
REFRESH_TOKEN,
REQUESTOR,
USERNAME,
two_factor_callback,
)


Expand Down Expand Up @@ -60,7 +61,9 @@ def script_authorizer():
authenticator = prawcore.TrustedAuthenticator(
REQUESTOR, CLIENT_ID, CLIENT_SECRET
)
authorizer = prawcore.ScriptAuthorizer(authenticator, USERNAME, PASSWORD)
authorizer = prawcore.ScriptAuthorizer(
authenticator, USERNAME, PASSWORD, two_factor_callback
)
authorizer.refresh()
return authorizer

Expand Down

0 comments on commit 437c2e4

Please sign in to comment.