Skip to content

Commit

Permalink
Merge pull request #1687 from praw-dev/callbacks
Browse files Browse the repository at this point in the history
Support single-use refresh tokens
  • Loading branch information
LilSpazJoekp authored Feb 24, 2021
2 parents daae2a2 + bb0e000 commit c74f4e4
Show file tree
Hide file tree
Showing 19 changed files with 501 additions and 159 deletions.
15 changes: 13 additions & 2 deletions CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,23 @@ Change Log
Unreleased
----------

**Added**

* :class:`.Reddit` keyword argument ``token_manager``.
* :class:`.FileTokenManager` and its parent abstract class :class:`.BaseTokenManager`.

**Deprecated**

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

7.1.4 (2021/02/07)
------------------
Expand Down
1 change: 1 addition & 0 deletions docs/code_overview/other.rst
Original file line number Diff line number Diff line change
Expand Up @@ -122,5 +122,6 @@ them bound to an attribute of one of the PRAW models.
other/subredditremovalreasons
other/subredditrules
other/redditorstream
other/token_manager
other/trophy
other/util
5 changes: 5 additions & 0 deletions docs/code_overview/other/token_manager.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
Token Manager
=============

.. automodule:: praw.util.token_manager
:inherited-members:
1 change: 1 addition & 0 deletions docs/examples/lmgtfy_bot.py
100644 → 100755
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
#!/usr/bin/env python3
from urllib.parse import quote_plus

import praw
Expand Down
90 changes: 42 additions & 48 deletions docs/examples/obtain_refresh_token.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,76 +2,48 @@

"""This example demonstrates the flow for retrieving a refresh token.
In order for this example to work your application's redirect URI must be set to
http://localhost:8080.
This tool can be used to conveniently create refresh tokens for later use with your web
application OAuth2 credentials.
"""
import random
import socket
import sys
To create a Reddit application visit the following link while logged into the account
you want to create a refresh token for: https://www.reddit.com/prefs/apps/
import praw
Create a "web app" with the redirect uri set to: http://localhost:8080
After the application is created, take note of:
def receive_connection():
"""Wait for and then return a connected socket..
- REDDIT_CLIENT_ID; the line just under "web app" in the upper left of the Reddit
Application
- REDDIT_CLIENT_SECRET; the value to the right of "secret"
Opens a TCP connection on port 8080, and waits for a single client.
Usage:
"""
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
server.bind(("localhost", 8080))
server.listen(1)
client = server.accept()[0]
server.close()
return client
EXPORT praw_client_id=<REDDIT_CLIENT_ID>
EXPORT praw_client_secret=<REDDIT_CLIENT_SECRET>
python3 obtain_refresh_token.py
"""
import random
import socket
import sys

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


def main():
"""Provide the program's entry point when directly executed."""
print(
"Go here while logged into the account you want to create a token for:"
" https://www.reddit.com/prefs/apps/"
scope_input = input(
"Enter a comma separated list of scopes, or `*` for all scopes: "
)
print(
"Click the create an app button. Put something in the name field and select the"
" script radio button."
)
print("Put http://localhost:8080 in the redirect uri field and click create app")
client_id = input(
"Enter the client ID, it's the line just under Personal use script at the top: "
)
client_secret = input("Enter the client secret, it's the line next to secret: ")
commaScopes = input(
"Now enter a comma separated list of scopes, or all for all tokens: "
)

if commaScopes.lower() == "all":
scopes = ["*"]
else:
scopes = commaScopes.strip().split(",")
scopes = [scope.strip() for scope in scope_input.strip().split(",")]

reddit = praw.Reddit(
client_id=client_id.strip(),
client_secret=client_secret.strip(),
redirect_uri="http://localhost:8080",
user_agent="praw_refresh_token_example",
user_agent="obtain_refresh_token/v0 by u/bboe",
)
state = str(random.randint(0, 65000))
url = reddit.auth.url(scopes, state, "permanent")
print(f"Now open this url in your browser: {url}")
sys.stdout.flush()

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


def receive_connection():
"""Wait for and then return a connected socket..
Opens a TCP connection on port 8080, and waits for a single client.
"""
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
server.bind(("localhost", 8080))
server.listen(1)
client = server.accept()[0]
server.close()
return client


def send_message(client, message):
"""Send message to client and close the connection."""
print(message)
client.send(f"HTTP/1.1 200 OK\r\n\r\n{message}".encode("utf-8"))
client.close()


if __name__ == "__main__":
sys.exit(main())
67 changes: 67 additions & 0 deletions docs/examples/use_file_token_manager.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
#!/usr/bin/env python3
"""This example demonstrates using the file token manager for refresh tokens.
In order to run this program, you will first need to obtain a valid refresh token. You
can use the `obtain_refresh_token.py` example to help.
In this example, refresh tokens will be saved into a file `refresh_token.txt` relative
to your current working directory. If your current working directory is under version
control it is strongly encouraged you add `refresh_token.txt` to the version control
ignore list.
Usage:
EXPORT praw_client_id=<REDDIT_CLIENT_ID>
EXPORT praw_client_secret=<REDDIT_CLIENT_SECRET>
python3 use_file_token_manager.py
"""
import os
import sys

import praw
from praw.util.token_manager import FileTokenManager

REFRESH_TOKEN_FILENAME = "refresh_token.txt"


def initialize_refresh_token_file():
if os.path.isfile(REFRESH_TOKEN_FILENAME):
return

refresh_token = input("Initial refresh token value: ")
with open(REFRESH_TOKEN_FILENAME, "w") as fp:
fp.write(refresh_token)


def main():
if "praw_client_id" not in os.environ:
sys.stderr.write("Environment variable ``praw_client_id`` must be defined\n")
return 1
if "praw_client_secret" not in os.environ:
sys.stderr.write(
"Environment variable ``praw_client_secret`` must be defined\n"
)
return 1

initialize_refresh_token_file()

refresh_token_manager = FileTokenManager(REFRESH_TOKEN_FILENAME)
reddit = praw.Reddit(
token_manager=refresh_token_manager,
user_agent="use_file_token_manager/v0 by u/bboe",
)

scopes = reddit.auth.scopes()
if scopes == {"*"}:
print(f"{reddit.user.me()} is authenticated with all scopes")
elif "identity" in scopes:
print(
f"{reddit.user.me()} is authenticated with the following scopes: {scopes}"
)
else:
print(f"You are authenticated with the following scopes: {scopes}")


if __name__ == "__main__":
sys.exit(main())
54 changes: 15 additions & 39 deletions docs/getting_started/authentication.rst
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
Authenticating via OAuth
========================

PRAW supports the three types of applications that can be registered on
PRAW supports all three types of applications that can be registered on
Reddit. Those are:

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

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

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

.. rst-class:: center_table_items

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

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

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

.. _auth_url:

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

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

.. code-block:: python
print(reddit.auth.authorize(code))
print(reddit.user.me())
The first line of output is the ``refresh_token``. You can save this for later use (see
:ref:`using_refresh_token`).
:ref:`using_refresh_tokens`).

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

The code flow can be used with an **installed** application just as described above with
one change: set the value of ``client_secret`` to ``None`` when initializing
Expand Down Expand Up @@ -256,28 +257,3 @@ such as in installed applications where the end user could retrieve the ``client
from each other (as the supplied device id *should* be a unique string per both
device (in the case of a web app, server) and user (in the case of a web app,
browser session).

.. _using_refresh_token:

Using a Saved Refresh Token
---------------------------

A saved refresh token can be used to immediately obtain an authorized instance of
:class:`.Reddit` like so:

.. code-block:: python
reddit = praw.Reddit(client_id="SI8pN3DSbt0zor",
client_secret="xaxkj7HNh8kwg8e5t4m6KvSrbTI",
refresh_token="WeheY7PwgeCZj4S3QgUcLhKE5S2s4eAYdxM",
user_agent="testscript by u/fakebot3"
)
print(reddit.auth.scopes())
The output from the above code displays which scopes are available on the
:class:`.Reddit` instance.

.. note::

Observe that ``redirect_uri`` does not need to be provided in such cases. It is only
needed when :meth:`.url` is used.
7 changes: 0 additions & 7 deletions docs/getting_started/configuration/options.rst
Original file line number Diff line number Diff line change
Expand Up @@ -38,13 +38,6 @@ OAuth Configuration Options
application. This option is required for all application types, however, the value
must be set to ``None`` for **installed** applications.

:refresh_token: For either **web** applications, or **installed** applications using the
code flow, you can directly provide a previously obtained refresh token. Using a
**web** application in conjunction with this option is useful, for example, if you
prefer to not have your username and password available to your program, as required
for a **script** application. See: :ref:`refresh_token` and
:ref:`using_refresh_token`

:redirect_uri: The redirect URI associated with your registered Reddit application. This
field is unused for **script** applications and is only needed for both **web**
applications, and **installed** applications when the :meth:`.url` method is used.
Expand Down
Loading

0 comments on commit c74f4e4

Please sign in to comment.