Skip to content

Commit c74f4e4

Browse files
authored
Merge pull request #1687 from praw-dev/callbacks
Support single-use refresh tokens
2 parents daae2a2 + bb0e000 commit c74f4e4

File tree

19 files changed

+501
-159
lines changed

19 files changed

+501
-159
lines changed

CHANGES.rst

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,23 @@ Change Log
44
Unreleased
55
----------
66

7+
**Added**
8+
9+
* :class:`.Reddit` keyword argument ``token_manager``.
10+
* :class:`.FileTokenManager` and its parent abstract class :class:`.BaseTokenManager`.
11+
712
**Deprecated**
813

9-
* :meth:`.me` will no longer return ``None`` when called in :attr:`.read_only` mode
14+
* The configuration setting ``refresh_token`` is deprecated and its use will result in a
15+
:py:class:`DeprecationWarning`. This deprecation applies in all ways of setting
16+
configuration values, i.e., via ``praw.ini``, as a keyword argument when initializing
17+
an instance of :class:`.Reddit`, and via the ``PRAW_REFRESH_TOKEN`` environment
18+
variable. To be prepared for PRAW 8, use the new :class:`.Reddit` keyword argument
19+
``token_manager``. See :ref:`refresh_token` in PRAW's documentation for an example.
20+
* :meth:`.me` will no longer return ``None`` when called in :attr:`.read_only` mode
1021
starting in PRAW 8. A :py:class:`DeprecationWarning` will be issued. To switch forward
1122
to the PRAW 8 behavior set ``praw8_raise_exception_on_me=True`` in your
12-
``praw.Reddit(...)`` call.
23+
:class:`.Reddit` call.
1324

1425
7.1.4 (2021/02/07)
1526
------------------

docs/code_overview/other.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,5 +122,6 @@ them bound to an attribute of one of the PRAW models.
122122
other/subredditremovalreasons
123123
other/subredditrules
124124
other/redditorstream
125+
other/token_manager
125126
other/trophy
126127
other/util
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
Token Manager
2+
=============
3+
4+
.. automodule:: praw.util.token_manager
5+
:inherited-members:

docs/examples/lmgtfy_bot.py

100644100755
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
#!/usr/bin/env python3
12
from urllib.parse import quote_plus
23

34
import praw

docs/examples/obtain_refresh_token.py

Lines changed: 42 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -2,76 +2,48 @@
22

33
"""This example demonstrates the flow for retrieving a refresh token.
44
5-
In order for this example to work your application's redirect URI must be set to
6-
http://localhost:8080.
7-
85
This tool can be used to conveniently create refresh tokens for later use with your web
96
application OAuth2 credentials.
107
11-
"""
12-
import random
13-
import socket
14-
import sys
8+
To create a Reddit application visit the following link while logged into the account
9+
you want to create a refresh token for: https://www.reddit.com/prefs/apps/
1510
16-
import praw
11+
Create a "web app" with the redirect uri set to: http://localhost:8080
1712
13+
After the application is created, take note of:
1814
19-
def receive_connection():
20-
"""Wait for and then return a connected socket..
15+
- REDDIT_CLIENT_ID; the line just under "web app" in the upper left of the Reddit
16+
Application
17+
- REDDIT_CLIENT_SECRET; the value to the right of "secret"
2118
22-
Opens a TCP connection on port 8080, and waits for a single client.
19+
Usage:
2320
24-
"""
25-
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
26-
server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
27-
server.bind(("localhost", 8080))
28-
server.listen(1)
29-
client = server.accept()[0]
30-
server.close()
31-
return client
21+
EXPORT praw_client_id=<REDDIT_CLIENT_ID>
22+
EXPORT praw_client_secret=<REDDIT_CLIENT_SECRET>
23+
python3 obtain_refresh_token.py
3224
25+
"""
26+
import random
27+
import socket
28+
import sys
3329

34-
def send_message(client, message):
35-
"""Send message to client and close the connection."""
36-
print(message)
37-
client.send(f"HTTP/1.1 200 OK\r\n\r\n{message}".encode("utf-8"))
38-
client.close()
30+
import praw
3931

4032

4133
def main():
4234
"""Provide the program's entry point when directly executed."""
43-
print(
44-
"Go here while logged into the account you want to create a token for:"
45-
" https://www.reddit.com/prefs/apps/"
35+
scope_input = input(
36+
"Enter a comma separated list of scopes, or `*` for all scopes: "
4637
)
47-
print(
48-
"Click the create an app button. Put something in the name field and select the"
49-
" script radio button."
50-
)
51-
print("Put http://localhost:8080 in the redirect uri field and click create app")
52-
client_id = input(
53-
"Enter the client ID, it's the line just under Personal use script at the top: "
54-
)
55-
client_secret = input("Enter the client secret, it's the line next to secret: ")
56-
commaScopes = input(
57-
"Now enter a comma separated list of scopes, or all for all tokens: "
58-
)
59-
60-
if commaScopes.lower() == "all":
61-
scopes = ["*"]
62-
else:
63-
scopes = commaScopes.strip().split(",")
38+
scopes = [scope.strip() for scope in scope_input.strip().split(",")]
6439

6540
reddit = praw.Reddit(
66-
client_id=client_id.strip(),
67-
client_secret=client_secret.strip(),
6841
redirect_uri="http://localhost:8080",
69-
user_agent="praw_refresh_token_example",
42+
user_agent="obtain_refresh_token/v0 by u/bboe",
7043
)
7144
state = str(random.randint(0, 65000))
7245
url = reddit.auth.url(scopes, state, "permanent")
7346
print(f"Now open this url in your browser: {url}")
74-
sys.stdout.flush()
7547

7648
client = receive_connection()
7749
data = client.recv(1024).decode("utf-8")
@@ -95,5 +67,27 @@ def main():
9567
return 0
9668

9769

70+
def receive_connection():
71+
"""Wait for and then return a connected socket..
72+
73+
Opens a TCP connection on port 8080, and waits for a single client.
74+
75+
"""
76+
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
77+
server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
78+
server.bind(("localhost", 8080))
79+
server.listen(1)
80+
client = server.accept()[0]
81+
server.close()
82+
return client
83+
84+
85+
def send_message(client, message):
86+
"""Send message to client and close the connection."""
87+
print(message)
88+
client.send(f"HTTP/1.1 200 OK\r\n\r\n{message}".encode("utf-8"))
89+
client.close()
90+
91+
9892
if __name__ == "__main__":
9993
sys.exit(main())
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
#!/usr/bin/env python3
2+
"""This example demonstrates using the file token manager for refresh tokens.
3+
4+
In order to run this program, you will first need to obtain a valid refresh token. You
5+
can use the `obtain_refresh_token.py` example to help.
6+
7+
In this example, refresh tokens will be saved into a file `refresh_token.txt` relative
8+
to your current working directory. If your current working directory is under version
9+
control it is strongly encouraged you add `refresh_token.txt` to the version control
10+
ignore list.
11+
12+
Usage:
13+
14+
EXPORT praw_client_id=<REDDIT_CLIENT_ID>
15+
EXPORT praw_client_secret=<REDDIT_CLIENT_SECRET>
16+
python3 use_file_token_manager.py
17+
18+
"""
19+
import os
20+
import sys
21+
22+
import praw
23+
from praw.util.token_manager import FileTokenManager
24+
25+
REFRESH_TOKEN_FILENAME = "refresh_token.txt"
26+
27+
28+
def initialize_refresh_token_file():
29+
if os.path.isfile(REFRESH_TOKEN_FILENAME):
30+
return
31+
32+
refresh_token = input("Initial refresh token value: ")
33+
with open(REFRESH_TOKEN_FILENAME, "w") as fp:
34+
fp.write(refresh_token)
35+
36+
37+
def main():
38+
if "praw_client_id" not in os.environ:
39+
sys.stderr.write("Environment variable ``praw_client_id`` must be defined\n")
40+
return 1
41+
if "praw_client_secret" not in os.environ:
42+
sys.stderr.write(
43+
"Environment variable ``praw_client_secret`` must be defined\n"
44+
)
45+
return 1
46+
47+
initialize_refresh_token_file()
48+
49+
refresh_token_manager = FileTokenManager(REFRESH_TOKEN_FILENAME)
50+
reddit = praw.Reddit(
51+
token_manager=refresh_token_manager,
52+
user_agent="use_file_token_manager/v0 by u/bboe",
53+
)
54+
55+
scopes = reddit.auth.scopes()
56+
if scopes == {"*"}:
57+
print(f"{reddit.user.me()} is authenticated with all scopes")
58+
elif "identity" in scopes:
59+
print(
60+
f"{reddit.user.me()} is authenticated with the following scopes: {scopes}"
61+
)
62+
else:
63+
print(f"You are authenticated with the following scopes: {scopes}")
64+
65+
66+
if __name__ == "__main__":
67+
sys.exit(main())

docs/getting_started/authentication.rst

Lines changed: 15 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
Authenticating via OAuth
44
========================
55

6-
PRAW supports the three types of applications that can be registered on
6+
PRAW supports all three types of applications that can be registered on
77
Reddit. Those are:
88

99
* `Web Applications <https://github.com/reddit-archive/reddit/wiki/OAuth2-App-Types#web-app>`_
@@ -14,10 +14,10 @@ Before you can use any one of these with PRAW, you must first `register
1414
<https://www.reddit.com/prefs/apps/>`_ an application of the appropriate type
1515
on Reddit.
1616

17-
If your app does not require a user context, it is :ref:`read-only <read_only_application>`.
17+
If your application does not require a user context, it is :ref:`read-only <read_only_application>`.
1818

1919
PRAW supports the flows that each of these applications can use. The
20-
following table defines which tables can use which flows:
20+
following table defines which application types can use which flows:
2121

2222
.. rst-class:: center_table_items
2323

@@ -131,12 +131,11 @@ When registering your application you must provide a valid redirect URI. If you
131131
running a website you will want to enter the appropriate callback URL and configure that
132132
endpoint to complete the code flow.
133133

134-
If you aren't actually running a website, you can use the :ref:`refresh_token` script to
135-
obtain ``refresh_tokens``. Enter ``http://localhost:8080`` as the redirect URI when
136-
using this script.
134+
If you aren't actually running a website, you can follow the :ref:`refresh_token`
135+
tutorial to learn how to obtain and use the initial refresh token.
137136

138-
Whether or not you use the script there are two processes involved in obtaining
139-
access or refresh tokens.
137+
Whether or not you follow the :ref:`refresh_token` tutorial there are two processes
138+
involved in obtaining access or refresh tokens.
140139

141140
.. _auth_url:
142141

@@ -155,24 +154,26 @@ URL. You can do that as follows:
155154
)
156155
print(reddit.auth.url(["identity"], "...", "permanent"))
157156
158-
The above will output an authorization URL for a permanent token that has only the
159-
`identity` scope. See :meth:`.url` for more information on these parameters.
157+
The above will output an authorization URL for a permanent token (i.e., the resulting
158+
authorization will include both a short-lived ``access_token``, and a longer-lived, single
159+
use ``refresh_token``) that has only the ``identity`` scope. See :meth:`.url` for more
160+
information on these parameters.
160161

161162
This URL should be accessed by the account that desires to authorize their Reddit access
162163
to your application. On completion of that flow, the user's browser will be redirected
163-
to the specified ``redirect_uri``. After extracting verifying the ``state`` and
164-
extracting the ``code`` you can obtain the refresh token via:
164+
to the specified ``redirect_uri``. After verifying the ``state`` and extracting the
165+
``code`` you can obtain the refresh token via:
165166

166167
.. code-block:: python
167168
168169
print(reddit.auth.authorize(code))
169170
print(reddit.user.me())
170171
171172
The first line of output is the ``refresh_token``. You can save this for later use (see
172-
:ref:`using_refresh_token`).
173+
:ref:`using_refresh_tokens`).
173174

174175
The second line of output reveals the name of the Redditor that completed the code flow.
175-
It also indicates that the ``reddit`` instance is now associated with that account.
176+
It also indicates that the :class:`.Reddit` instance is now associated with that account.
176177

177178
The code flow can be used with an **installed** application just as described above with
178179
one change: set the value of ``client_secret`` to ``None`` when initializing
@@ -256,28 +257,3 @@ such as in installed applications where the end user could retrieve the ``client
256257
from each other (as the supplied device id *should* be a unique string per both
257258
device (in the case of a web app, server) and user (in the case of a web app,
258259
browser session).
259-
260-
.. _using_refresh_token:
261-
262-
Using a Saved Refresh Token
263-
---------------------------
264-
265-
A saved refresh token can be used to immediately obtain an authorized instance of
266-
:class:`.Reddit` like so:
267-
268-
.. code-block:: python
269-
270-
reddit = praw.Reddit(client_id="SI8pN3DSbt0zor",
271-
client_secret="xaxkj7HNh8kwg8e5t4m6KvSrbTI",
272-
refresh_token="WeheY7PwgeCZj4S3QgUcLhKE5S2s4eAYdxM",
273-
user_agent="testscript by u/fakebot3"
274-
)
275-
print(reddit.auth.scopes())
276-
277-
The output from the above code displays which scopes are available on the
278-
:class:`.Reddit` instance.
279-
280-
.. note::
281-
282-
Observe that ``redirect_uri`` does not need to be provided in such cases. It is only
283-
needed when :meth:`.url` is used.

docs/getting_started/configuration/options.rst

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -38,13 +38,6 @@ OAuth Configuration Options
3838
application. This option is required for all application types, however, the value
3939
must be set to ``None`` for **installed** applications.
4040

41-
:refresh_token: For either **web** applications, or **installed** applications using the
42-
code flow, you can directly provide a previously obtained refresh token. Using a
43-
**web** application in conjunction with this option is useful, for example, if you
44-
prefer to not have your username and password available to your program, as required
45-
for a **script** application. See: :ref:`refresh_token` and
46-
:ref:`using_refresh_token`
47-
4841
:redirect_uri: The redirect URI associated with your registered Reddit application. This
4942
field is unused for **script** applications and is only needed for both **web**
5043
applications, and **installed** applications when the :meth:`.url` method is used.

0 commit comments

Comments
 (0)