Repository: praw-dev/praw

Issue:
Code Flow seems broken for Refresh Tokens
## Issue Description

I had reached out a while ago via Gitter for this, and it seemed to be an issue, but I figured I should document it here:

Using the default guide for Installed Applications, [found here](https://praw.readthedocs.io/en/latest/getting_started/authentication.html#installed-application), I seem unable to authenticate for a refresh token using an Installed App ID. The issue is Praw raising the exception ```praw.exceptions.ClientException: authorize can only be used with web apps.```, which doesn't seem correct.

I may be missing something here, so I've included my test code: https://gist.github.com/shadowmoose/5ed7eb23c08b354fd2f7918ac8b1043d

I've also included another (ugly) test script, which demonstrates the ability to get a refresh token from the same input, but also seems to fail (with a recursion error) in Praw even when the refresh token is directly provided: https://gist.github.com/shadowmoose/42131b3830f64e5025ead87cb54c982a

Apologies if I'm just doing something incorrectly, but it looks like nobody on the official Gitter was able to spot my error.

## System Information

- PRAW Version: 5.4.0
- Python Version: 3.6.3+
- Operating System: Windows, Linux



Discussion:
You're right: installed app can get refresh token from reddit using empty client secret. [Demonstations is here.](https://gist.github.com/nmtake/251b6ece0965667334a671ab13b5be7b)

I think commenting out these lines will fix the issue. Could you try to test that?

https://github.com/praw-dev/praw/blob/a3820a24f77f6094e6985df86ae094307957b8f6/praw/models/auth.py#L48-L50
Same issue, said I'd make a patch a long while ago but then ended up getting caught up in other things >.<. 

There are other issues in regards to the logic of what apps can and can not use different flows as well, but I forget which inconsistencies occur. I had to make two emtire flow charts just to start seeing what's for what due to poorish documentation on reddit's OAuth2 page.
@nmtake Can confirm that once the exception is removed, the authorization seems to work correctly. At least so far as testing with ```reddit.auth.scopes()``` and ```reddit.user.me()``` goes. 

I haven't had time to dig around in the praw source, but this seems to be an issue with the assumptions made by the library there.
@13steinj At least all three app types (web, script, installed) can use authorization code grant flow
and can get optional refresh token. 

@shadowmoose Thanks. I'll make a PR for the issue in this week (hopefully).

Resolving patch:
diff --git a/praw/models/auth.py b/praw/models/auth.py
--- a/praw/models/auth.py
+++ b/praw/models/auth.py
@@ -1,6 +1,6 @@
 """Provide the Auth class."""
-from prawcore import (Authorizer, ImplicitAuthorizer, TrustedAuthenticator,
-                      UntrustedAuthenticator, session)
+from prawcore import (Authorizer, ImplicitAuthorizer, UntrustedAuthenticator,
+                      session)
 
 from .base import PRAWBase
 from ..exceptions import ClientException
@@ -45,9 +45,6 @@ def authorize(self, code):
 
         """
         authenticator = self._reddit._read_only_core._authorizer._authenticator
-        if not isinstance(authenticator, TrustedAuthenticator) or \
-           self._reddit.config.username:
-            raise ClientException('authorize can only be used with web apps.')
         authorizer = Authorizer(authenticator)
         authorizer.authorize(code)
         authorized_session = session(authorizer)
diff --git a/praw/reddit.py b/praw/reddit.py
--- a/praw/reddit.py
+++ b/praw/reddit.py
@@ -349,7 +349,12 @@ def _prepare_untrusted_prawcore(self, requestor):
                                                self.config.client_id,
                                                self.config.redirect_uri)
         read_only_authorizer = DeviceIDAuthorizer(authenticator)
-        self._core = self._read_only_core = session(read_only_authorizer)
+        self._read_only_core = session(read_only_authorizer)
+        if self.config.refresh_token:
+            authorizer = Authorizer(authenticator, self.config.refresh_token)
+            self._core = self._authorized_core = session(authorizer)
+        else:
+            self._core = self._read_only_core
 
     def comment(self,  # pylint: disable=invalid-name
                 id=None,  # pylint: disable=redefined-builtin


Test patch:
diff --git a/tests/unit/models/test_auth.py b/tests/unit/models/test_auth.py
--- a/tests/unit/models/test_auth.py
+++ b/tests/unit/models/test_auth.py
@@ -12,6 +12,11 @@ def installed_app():
 
 
 def script_app():
+    return Reddit(client_id='dummy client', client_secret='dummy secret',
+                  redirect_uri='https://dummy.tld/', user_agent='dummy')
+
+
+def script_app_with_password():
     return Reddit(client_id='dummy client', client_secret='dummy secret',
                   password='dummy password', user_agent='dummy',
                   username='dummy username')
@@ -23,21 +28,20 @@ def web_app():
 
 
 class TestAuth(UnitTest):
-    def test_authorize__from_installed_app(self):
+    def test_implicit__from_script_app(self):
         with pytest.raises(ClientException):
-            installed_app().auth.authorize('dummy code')
-
-    def test_authorize__from_script_app(self):
+            script_app().auth.implicit('dummy token', 10, '')
         with pytest.raises(ClientException):
-            script_app().auth.authorize('dummy code')
+            script_app_with_password().auth.implicit('dummy token', 10, '')
 
-    def test_implicit__from_script_app(self):
+    def test_implicit__from_web_app(self):
         with pytest.raises(ClientException):
-            script_app().auth.implicit('dummy token', 10, '')
+            web_app().auth.implicit('dummy token', 10, '')
 
     def test_limits(self):
         expected = {'remaining': None, 'reset_timestamp': None, 'used': None}
-        for app in [installed_app(), script_app(), web_app()]:
+        for app in [installed_app(), script_app(), script_app_with_password(),
+                    web_app()]:
             assert expected == app.auth.limits
 
     def test_url__installed_app(self):
diff --git a/tests/unit/test_reddit.py b/tests/unit/test_reddit.py
--- a/tests/unit/test_reddit.py
+++ b/tests/unit/test_reddit.py
@@ -78,6 +78,16 @@ def test_read_only__with_authenticated_core(self):
             reddit.read_only = False
             assert not reddit.read_only
 
+    def test_read_only__with_authenticated_core__non_confidential(self):
+        with Reddit(client_id='dummy', client_secret=None,
+                    redirect_uri='dummy', user_agent='dummy',
+                    refresh_token='dummy') as reddit:
+            assert not reddit.read_only
+            reddit.read_only = True
+            assert reddit.read_only
+            reddit.read_only = False
+            assert not reddit.read_only
+
     def test_read_only__with_script_authenticated_core(self):
         with Reddit(password='dummy', username='dummy',
                     **self.REQUIRED_DUMMY_SETTINGS) as reddit: