Skip to content

Commit 1f7497c

Browse files
author
Hugo
authored
Merge pull request #985 from telotortium/gcal-oauth-remove-oob
gcal: replace oob OAuth2 with local server redirect
2 parents 7c2fed1 + baaf737 commit 1f7497c

File tree

2 files changed

+91
-4
lines changed

2 files changed

+91
-4
lines changed

vdirsyncer/storage/google.py

Lines changed: 36 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,10 @@
22
import logging
33
import os
44
import urllib.parse as urlparse
5+
import wsgiref.simple_server
6+
import wsgiref.util
57
from pathlib import Path
8+
from threading import Thread
69

710
import aiohttp
811
import click
@@ -14,6 +17,8 @@
1417
from ..utils import open_graphical_browser
1518
from . import base
1619
from . import dav
20+
from .google_helpers import _RedirectWSGIApp
21+
from .google_helpers import _WSGIRequestHandler
1722

1823
logger = logging.getLogger(__name__)
1924

@@ -54,6 +59,7 @@ def __init__(
5459
self._client_id = client_id
5560
self._client_secret = client_secret
5661
self._token = None
62+
self._redirect_uri = None
5763

5864
async def request(self, method, path, **kwargs):
5965
if not self._token:
@@ -69,12 +75,18 @@ async def _save_token(self, token):
6975

7076
@property
7177
def _session(self):
72-
"""Return a new OAuth session for requests."""
78+
"""Return a new OAuth session for requests.
79+
80+
Accesses the self.redirect_uri field (str): the URI to redirect
81+
authentication to. Should be a loopback address for a local server that
82+
follows the process detailed in
83+
https://developers.google.com/identity/protocols/oauth2/native-app.
84+
"""
7385

7486
return OAuth2Session(
7587
client_id=self._client_id,
7688
token=self._token,
77-
redirect_uri="urn:ietf:wg:oauth:2.0:oob",
89+
redirect_uri=self._redirect_uri,
7890
scope=self.scope,
7991
auto_refresh_url=REFRESH_URL,
8092
auto_refresh_kwargs={
@@ -102,7 +114,18 @@ async def _init_token(self):
102114
# Some times a task stops at this `async`, and another continues the flow.
103115
# At this point, the user has already completed the flow, but is prompeted
104116
# for a second one.
117+
wsgi_app = _RedirectWSGIApp("Successfully obtained token.")
118+
wsgiref.simple_server.WSGIServer.allow_reuse_address = False
119+
host = "127.0.0.1"
120+
local_server = wsgiref.simple_server.make_server(
121+
host, 0, wsgi_app, handler_class=_WSGIRequestHandler
122+
)
123+
thread = Thread(target=local_server.handle_request)
124+
thread.start()
125+
self._redirect_uri = f"http://{host}:{local_server.server_port}"
105126
async with self._session as session:
127+
# Fail fast if the address is occupied
128+
106129
authorization_url, state = session.authorization_url(
107130
TOKEN_URL,
108131
# access_type and approval_prompt are Google specific
@@ -117,14 +140,23 @@ async def _init_token(self):
117140
logger.warning(str(e))
118141

119142
click.echo("Follow the instructions on the page.")
120-
code = click.prompt("Paste obtained code")
143+
thread.join()
144+
logger.debug("server handled request!")
121145

146+
# Note: using https here because oauthlib is very picky that
147+
# OAuth 2.0 should only occur over https.
148+
authorization_response = wsgi_app.last_request_uri.replace(
149+
"http", "https", 1
150+
)
151+
logger.debug(f"authorization_response: {authorization_response}")
122152
self._token = await session.fetch_token(
123153
REFRESH_URL,
124-
code=code,
154+
authorization_response=authorization_response,
125155
# Google specific extra param used for client authentication:
126156
client_secret=self._client_secret,
127157
)
158+
logger.debug(f"token: {self._token}")
159+
local_server.server_close()
128160

129161
# FIXME: Ugly
130162
await self._save_token(self._token)

vdirsyncer/storage/google_helpers.py

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
# SPDX-License-Identifier: Apache-2.0
2+
#
3+
# Based on:
4+
# https://github.com/googleapis/google-auth-library-python-oauthlib/blob/1fb16be1bad9050ee29293541be44e41e82defd7/google_auth_oauthlib/flow.py#L513
5+
6+
import logging
7+
import wsgiref.simple_server
8+
import wsgiref.util
9+
from typing import Any
10+
from typing import Callable
11+
from typing import Dict
12+
from typing import Iterable
13+
from typing import Optional
14+
15+
logger = logging.getLogger(__name__)
16+
17+
18+
class _WSGIRequestHandler(wsgiref.simple_server.WSGIRequestHandler):
19+
"""Custom WSGIRequestHandler."""
20+
21+
def log_message(self, format, *args):
22+
# (format is the argument name defined in the superclass.)
23+
logger.info(format, *args)
24+
25+
26+
class _RedirectWSGIApp:
27+
"""WSGI app to handle the authorization redirect.
28+
29+
Stores the request URI and displays the given success message.
30+
"""
31+
32+
last_request_uri: Optional[str]
33+
34+
def __init__(self, success_message: str):
35+
"""
36+
:param success_message: The message to display in the web browser the
37+
authorization flow is complete.
38+
"""
39+
self.last_request_uri = None
40+
self._success_message = success_message
41+
42+
def __call__(
43+
self,
44+
environ: Dict[str, Any],
45+
start_response: Callable[[str, list], None],
46+
) -> Iterable[bytes]:
47+
"""WSGI Callable.
48+
49+
:param environ: The WSGI environment.
50+
:param start_response: The WSGI start_response callable.
51+
:returns: The response body.
52+
"""
53+
start_response("200 OK", [("Content-type", "text/plain; charset=utf-8")])
54+
self.last_request_uri = wsgiref.util.request_uri(environ)
55+
return [self._success_message.encode("utf-8")]

0 commit comments

Comments
 (0)