If like me, you got a prawcore.exceptions.OAuthException: invalid_grant error processing request
error out of the blue for your reddit bot that had been running without an issue for years on AWS Lambda, then maybe like me you also enabled 2FA on your Reddit account recently and failed to realize that the change would affect your reddit bot too since it authenticates to Reddit using your credentials.
To the best of my knowledge, reddit doesn't have 'App Passwords' like Google does so you'll have to find a way to supply the OTP code every time you authenticate to Reddit using PRAW.
Thanks to this pull request, prawcore
can now accept 2FA codes for ScriptAuthorizer
via a callback function.
To get the OTP code, you can use pyotp. The following sample code is from the pyotp
project page:
totp = pyotp.TOTP('base32secret3232')
totp.now() # => '492039'
# OTP verified for current time
totp.verify('492039') # => True
time.sleep(30)
totp.verify('492039') # => False
To get the TOTP secret if you don't have it, you could disable 2FA and enable it again to get it or if you use Authy as your 2FA Authenticator app, you can refer to this gist about exporting TOTP tokens from Authy Desktop app.
Once you have everything you need, refer to the sample code below:
Old code (No 2FA - doesn't work):
import praw
reddit = praw.Reddit(
client_id=PRAW_CLIENT_ID,
client_secret=PRAW_SECRET, password=REDDIT_PASSWORD,
user_agent=REDDIT_USERAGENT, username=REDDIT_USERNAME
)
print(list(reddit.user.blocked())) # fails
Old code modifed to supply OTP code along with REDDIT_PASSWORD
:
import praw, prawcore, pyotp
totp = pyotp.TOTP('REDDIT_TOTP_SECRET')
reddit = praw.Reddit(
client_id=PRAW_CLIENT_ID,
client_secret=PRAW_SECRET, password="{}:{}".format(REDDIT_PASSWORD, totp.now()),
user_agent=REDDIT_USERAGENT, username=REDDIT_USERNAME
)
print(list(reddit.user.blocked())) # works
New code with 2FA callback function:
import praw, prawcore, pyotp
totp = pyotp.TOTP('REDDIT_TOTP_SECRET')
def create_prawcore_two_factor_script_authorizer(two_factor_callback=None):
authenticator = prawcore.TrustedAuthenticator(
prawcore.Requestor(REDDIT_USERAGENT),
PRAW_CLIENT_ID,
PRAW_SECRET,
)
authorizer = prawcore.ScriptAuthorizer(
authenticator, REDDIT_USERNAME, REDDIT_PASSWORD, two_factor_callback
)
authorizer.refresh()
return authorizer
authorizer_2fa = create_prawcore_two_factor_script_authorizer(totp.now)
reddit = praw.Reddit(
client_id='fakeclient',
user_agent='fakeagent',
client_secret='fakesecret'
)
reddit._core._authorizer = authorizer_2fa
print(list(reddit.user.blocked())) # works
With both versions, you have to handle prawcore.exceptions.OAuthException
which will be raised for API calls made after an hour has passed, as mentioned in the PRAW docs