{"action":"created","issue":{"url":"https://api.github.com/repos/singingwolfboy/flask-dance/issues/35","labels_url":"https://api.github.com/repos/singingwolfboy/flask-dance/issues/35/labels{/name}","comments_url":"https://api.github.com/repos/singingwolfboy/flask-dance/issues/35/comments","events_url":"https://api.github.com/repos/singingwolfboy/flask-dance/issues/35/events","html_url":"https://github.com/singingwolfboy/flask-dance/issues/35","id":119227639,"number":35,"title":"oauthlib.oauth2.rfc6749.errors.TokenExpiredError","user":{"login":"rmoorman","id":219695,"avatar_url":"https://avatars.githubusercontent.com/u/219695?v=3","gravatar_id":"","url":"https://api.github.com/users/rmoorman","html_url":"https://github.com/rmoorman","followers_url":"https://api.github.com/users/rmoorman/followers","following_url":"https://api.github.com/users/rmoorman/following{/other_user}","gists_url":"https://api.github.com/users/rmoorman/gists{/gist_id}","starred_url":"https://api.github.com/users/rmoorman/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/rmoorman/subscriptions","organizations_url":"https://api.github.com/users/rmoorman/orgs","repos_url":"https://api.github.com/users/rmoorman/repos","events_url":"https://api.github.com/users/rmoorman/events{/privacy}","received_events_url":"https://api.github.com/users/rmoorman/received_events","type":"User","site_admin":false},"labels":[],"state":"open","locked":false,"assignee":null,"milestone":null,"comments":2,"created_at":"2015-11-27T17:21:11Z","updated_at":"2015-11-30T12:32:49Z","closed_at":null,"body":"When using the google auth example setup code, an `oauthlib.oauth2.rfc6749.errors.TokenExpiredError` is being thrown in case the access token received is expired which causes an internal server error to be shown.\r\n\r\nI suppose that exception should be handled by flask-dance itself by catching it and in case of a missing refresh token as seen here, trashing the auth info and probably initiating re-authentication.\r\nI did not test however what happens with forced approval prompt and offline access type (as further described here http://googlecode.blogspot.nl/2011/10/upcoming-changes-to-oauth-20-endpoint.html and on various other places of course..) which forces refresh token passing upon authentication.\r\n\r\n### How to reproduce\r\n\r\n1) create new project with a fresh set of credentials using the developer console of google with correct redirect urls (http://localhost:5000/login/google/authorized for example)\r\n\r\n2) add the credentials to the google auth example code\r\nMy example code so far (with added logging and an adjustment to let it work over plain HTTP for the sake of this example and dumping the whole user info instead of just the email) is:\r\n~~~~.python\r\nfrom flask import Flask, redirect, url_for\r\nfrom flask_dance.contrib.google import make_google_blueprint, google\r\n\r\napp = Flask(__name__)\r\napp.secret_key = \"supersekrit\"\r\napp.debug = True\r\nimport os\r\nos.environ[\"OAUTHLIB_INSECURE_TRANSPORT\"] = \"1\"\r\n\r\nblueprint = make_google_blueprint(\r\n    client_id=\"my-key-here\",\r\n    client_secret=\"my-secret-here\",\r\n    scope=[\"profile\", \"email\"]\r\n)\r\napp.register_blueprint(blueprint, url_prefix=\"/login\")\r\n\r\n# Some logging setup\r\nimport logging\r\nstream_handler = logging.StreamHandler()\r\nformatter = logging.Formatter(\r\n    \"[%(asctime)s] [%(process)d] [%(levelname)s] [%(name)s]: \"\r\n    \"%(pathname)s:%(lineno)d - \"\r\n    \"%(message)s\"\r\n)\r\nstream_handler.setFormatter(formatter)\r\napp.logger.addHandler(stream_handler)\r\napp.logger.info(\"app started\")\r\nlogging.basicConfig(level=logging.DEBUG)\r\n\r\n@app.route(\"/\")\r\ndef index():\r\n    if not google.authorized:\r\n        return redirect(url_for(\"google.login\"))\r\n    resp = google.get(\"https://www.googleapis.com/oauth2/v1/userinfo\")\r\n    assert resp.ok, resp.text\r\n    return resp.text\r\n\r\nif __name__ == \"__main__\":\r\n    app.run()\r\n~~~~\r\n\r\n3) start the app and authenticate once (maybe using an incognito browser session). Over here this causes the following output:\r\n~~~~~\r\nINFO:werkzeug:127.0.0.1 - - [27/Nov/2015 15:03:16] \"GET / HTTP/1.1\" 302 - \r\nDEBUG:requests_oauthlib.oauth2_session:Generated new state xxx. \r\nhttps://accounts.google.com/o/oauth2/auth?response_type=code&client_id=xxx.apps.googleusercontent.com&redirect_uri=http%3A%2F%2Flocalhost%3A5000%2Flogin%2Fgoogle%2Fauthorized&scope=profile+email&state=xxx \r\nINFO:werkzeug:127.0.0.1 - - [27/Nov/2015 15:03:16] \"GET /login/google HTTP/1.1\" 302 - \r\nDEBUG:requests_oauthlib.oauth2_session:Requesting url https://accounts.google.com/o/oauth2/token using method POST. \r\nDEBUG:requests_oauthlib.oauth2_session:Supplying headers {'Accept': 'application/json', 'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8'} and data {'redirect_uri': 'http://localhost:5000/login/google/authorized', 'code': 'xxx', 'client_id': 'xxx.apps.googleusercontent.com', 'client_secret': 'xxx', 'grant_type': 'authorization_code'} \r\nDEBUG:requests_oauthlib.oauth2_session:Passing through key word arguments {'timeout': None, 'verify': True, 'json': None, 'auth': None}. \r\nINFO:requests.packages.urllib3.connectionpool:Starting new HTTPS connection (1): accounts.google.com \r\nDEBUG:requests.packages.urllib3.connectionpool:\"POST /o/oauth2/token HTTP/1.1\" 200 None \r\nDEBUG:requests_oauthlib.oauth2_session:Prepared fetch token request body grant_type=authorization_code&code=xxx&client_id=xxx.apps.googleusercontent.com&client_secret=xxx&redirect_uri=http%3A%2F%2Flocalhost%3A5000%2Flogin%2Fgoogle%2Fauthorized \r\nDEBUG:requests_oauthlib.oauth2_session:Request to fetch token completed with status 200. \r\nDEBUG:requests_oauthlib.oauth2_session:Request headers were {'Content-Length': '277', 'Accept': 'application/json', 'Connection': 'keep-alive', 'User-Agent': 'python-requests/2.8.1', 'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8', 'Accept-Encoding': 'gzip, deflate'} \r\nDEBUG:requests_oauthlib.oauth2_session:Request body was redirect_uri=http%3A%2F%2Flocalhost%3A5000%2Flogin%2Fgoogle%2Fauthorized&code=xxx&client_id=xxx.apps.googleusercontent.com&client_secret=xxx&grant_type=authorization_code \r\nDEBUG:requests_oauthlib.oauth2_session:Response headers were {'Expires': 'Fri, 01 Jan 1990 00:00:00 GMT', 'Alt-Svc':\r\n'quic=\":443\"; ma=604800; v=\"30,29,28,27,26,25\"', 'X-Frame-Options': 'SAMEORIGIN', 'X-Content-Type-Options': 'nosniff', 'Content-Disposition': 'attachment; filename=\"json.txt\"; filename*=UTF-8\\'\\'json.txt', 'Transfer-Encoding': 'chunked', 'Date': 'Fri, 27 Nov 2015 14:04:37 GMT', 'Server': 'GSE', 'Pragma': 'no-cache', 'Content-Encoding': 'gzip', 'X-XSS-Protection': '1; mode=block', 'Alternate-Protocol': '443:quic,p=1', 'Set-Cookie': 'xxx', 'Cache-Control': 'no-cache, no-store, max-age=0,\r\nmust-revalidate', 'P3P': 'CP=\"This is not a P3P policy! See https://support.google.com/accounts/answer/151657?hl=en for more info.\"', 'Content-Type': 'application/json; charset=utf-8'} and content { \r\n \"access_token\" : \"xxx\", \r\n \"token_type\" : \"Bearer\", \r\n \"expires_in\" : 3600, \r\n \"id_token\" : \"xxx\" \r\n}. \r\nDEBUG:requests_oauthlib.oauth2_session:Invoking 0 token response hooks. \r\nDEBUG:requests_oauthlib.oauth2_session:Obtained token {'access_token': 'xxx', 'id_token': 'xxx', 'expires_at': 1448636677.8191292, 'expires_in': 3600, 'token_type': 'Bearer'}. \r\nINFO:werkzeug:127.0.0.1 - - [27/Nov/2015 15:04:37] \"GET /login/google/authorized?state=xxx&code=xxx HTTP/1.1\" 302 - \r\nDEBUG:requests_oauthlib.oauth2_session:Invoking 0 protected resource request hooks. \r\nDEBUG:requests_oauthlib.oauth2_session:Adding token {'id_token': 'xxx', 'expires_in': 3600, 'expires_at': 1448636677.8191292, 'access_token': 'xxx', 'token_type': 'Bearer'} to request. \r\nDEBUG:requests_oauthlib.oauth2_session:Requesting url https://www.googleapis.com/oauth2/v1/userinfo using method GET. \r\nDEBUG:requests_oauthlib.oauth2_session:Supplying headers {'Authorization': 'Bearer xxx'} and data None \r\nDEBUG:requests_oauthlib.oauth2_session:Passing through key word arguments {'allow_redirects': True}. \r\nINFO:requests.packages.urllib3.connectionpool:Starting new HTTPS connection (1): www.googleapis.com \r\nDEBUG:requests.packages.urllib3.connectionpool:\"GET /oauth2/v1/userinfo HTTP/1.1\" 200 None \r\nINFO:werkzeug:127.0.0.1 - - [27/Nov/2015 15:04:38] \"GET / HTTP/1.1\" 200 -\r\n~~~~~\r\n\r\n4) forget about it for more than an hour (expires in 3600 seconds in my case)\r\n\r\n5) refresh the page and see the exception `oauthlib.oauth2.rfc6749.errors.TokenExpiredError: (token_expired)`\r\n\r\n"},"comment":{"url":"https://api.github.com/repos/singingwolfboy/flask-dance/issues/comments/160617138","html_url":"https://github.com/singingwolfboy/flask-dance/issues/35#issuecomment-160617138","issue_url":"https://api.github.com/repos/singingwolfboy/flask-dance/issues/35","id":160617138,"user":{"login":"rmoorman","id":219695,"avatar_url":"https://avatars.githubusercontent.com/u/219695?v=3","gravatar_id":"","url":"https://api.github.com/users/rmoorman","html_url":"https://github.com/rmoorman","followers_url":"https://api.github.com/users/rmoorman/followers","following_url":"https://api.github.com/users/rmoorman/following{/other_user}","gists_url":"https://api.github.com/users/rmoorman/gists{/gist_id}","starred_url":"https://api.github.com/users/rmoorman/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/rmoorman/subscriptions","organizations_url":"https://api.github.com/users/rmoorman/orgs","repos_url":"https://api.github.com/users/rmoorman/repos","events_url":"https://api.github.com/users/rmoorman/events{/privacy}","received_events_url":"https://api.github.com/users/rmoorman/received_events","type":"User","site_admin":false},"created_at":"2015-11-30T12:32:48Z","updated_at":"2015-11-30T12:32:48Z","body":"Hello @singingwolfboy, I am glad if I can be of any help and good that you found the information useful.\r\nWhat I am wondering is, thinking of a possible solution to the problem, what other cases would have to lead to a redirect to the login route.\r\n\r\nMaybe something along the lines of this?\r\n~~~~.python\r\nfrom oauthlib.oauth2.rfc6749.errors import InvalidGrantError, TokenExpiredError, OAuth2Error\r\n\r\nclass OAuth2Session(BaseOAuth2Session):\r\n    # ....\r\n    def request(self, *args, **kwargs):\r\n        try:\r\n            return super(CustomOAuth2Session, self).request(*args, **kwargs)\r\n            # ... and other logic of course already in place here\r\n        except (InvalidGrantError, TokenExpiredError) as e: # or maybe any OAuth2Error\r\n            raise AuthenticationError from e\r\n~~~~\r\nThat logic would go into the already existing OAuth2Session class available and could intercept this and other errors and wrap them within a flask-dance specific exception type. Then, we could add some logic within the blueprint endpoints to catch those errors. But that still leaves us with those errors in views outside the blueprint.\r\nMaybe the AuthenticationError could be a custom HTTPException with a redirect response? The session which throws the error knows about the blueprint so we could build a suitable redirect response, give it to the custom implementation of HTTPException and raise it from within the CustomOAuth2Session.\r\n\r\nDo you think that this would be feasible and a good way to handle the issue within flask-dance?\r\n\r\nhttps://github.com/singingwolfboy/flask-dance/blob/master/flask_dance/consumer/requests.py#L86\r\nhttps://github.com/requests/requests-oauthlib/blob/master/requests_oauthlib/oauth2_session.py#L267\r\nhttps://github.com/idan/oauthlib/blob/master/oauthlib/oauth2/rfc6749/errors.py#L84\r\nhttp://werkzeug.pocoo.org/docs/0.11/exceptions/#werkzeug.exceptions.HTTPException"}}