From 87d975530eb214217ffe085b356a86ae596ff102 Mon Sep 17 00:00:00 2001 From: Gaganpreet Arora Date: Thu, 13 Apr 2023 23:31:04 +0200 Subject: [PATCH 1/6] Google oauth handler using an external Python script --- package/contents/scripts/google_redirect.py | 69 +++++++++++++++++++ .../ui/config/ConfigGoogleCalendar.qml | 63 +---------------- .../contents/ui/config/GoogleLoginManager.qml | 68 +++++++++--------- 3 files changed, 104 insertions(+), 96 deletions(-) create mode 100755 package/contents/scripts/google_redirect.py diff --git a/package/contents/scripts/google_redirect.py b/package/contents/scripts/google_redirect.py new file mode 100755 index 00000000..3282db54 --- /dev/null +++ b/package/contents/scripts/google_redirect.py @@ -0,0 +1,69 @@ +"""Script to handle oauth redirects from Google""" + +import json +import urllib.parse +import urllib.request +import urllib.error +import argparse +from http.server import BaseHTTPRequestHandler, HTTPServer +from urllib.parse import urlparse, parse_qs + +client_id = client_secret = listen_port = None + + +class OAuthRedirectHandler(BaseHTTPRequestHandler): + def do_GET(self): + query = urlparse(self.path).query + params = parse_qs(query) + # handle OAuth redirect here + if "code" in params: + code = params["code"][0] + # Exchange code for token from https://oauth2.googleapis.com/token + # using the following POST request: + token_params = { + "code": code, + "client_id": client_id, + "client_secret": client_secret, + "redirect_uri": "http://127.0.0.1:{}/".format(listen_port), + "grant_type": "authorization_code", + } + data = urllib.parse.urlencode(token_params).encode("utf-8") + try: + req = urllib.request.Request( + "https://oauth2.googleapis.com/token", data + ) + response = urllib.request.urlopen(req) + except urllib.error.HTTPError as e: + print(e.read().decode("utf-8")) + self.wfile.write(b"Handling redirect failed.") + raise SystemExit(1) + + # Parse the response and extract the access token + token_data = json.loads(response.read().decode("utf-8")) + with open("/tmp/token.json", "w") as f: + json.dump(token_data, f) + print(json.dumps(token_data, sort_keys=True)) + + self.send_response(200) + self.send_header("Content-type", "text/html") + self.end_headers() + self.wfile.write( + b"OAuth redirect handled successfully. You can close this tab now." + ) + # Exit the server + raise SystemExit(0) + + +if __name__ == "__main__": + parser = argparse.ArgumentParser() + parser.add_argument("--client_id", required=True) + parser.add_argument("--client_secret", required=True) + parser.add_argument("--listen_port", required=True, type=int) + args = parser.parse_args() + client_id = args.client_id + client_secret = args.client_secret + listen_port = args.listen_port + + server_address = ("", listen_port) + httpd = HTTPServer(server_address, OAuthRedirectHandler) + httpd.serve_forever() diff --git a/package/contents/ui/config/ConfigGoogleCalendar.qml b/package/contents/ui/config/ConfigGoogleCalendar.qml index a2109048..efeeb2fe 100644 --- a/package/contents/ui/config/ConfigGoogleCalendar.qml +++ b/package/contents/ui/config/ConfigGoogleCalendar.qml @@ -114,72 +114,15 @@ ConfigPage { visible: !googleLoginManager.isLoggedIn Label { Layout.fillWidth: true - text: i18n("To sync with Google Calendar") + text: i18n("To sync with Google Calendar click the button to first authorize your account.") color: readableNegativeTextColor wrapMode: Text.Wrap } - LinkText { - Layout.fillWidth: true - text: i18n("Visit %2 (opens in your web browser). After you login and give permission to access your calendar, it will give you a code to paste below.", googleLoginManager.authorizationCodeUrl, 'https://accounts.google.com/...') - color: readableNegativeTextColor - wrapMode: Text.Wrap - - // Tooltip - // QQC2.ToolTip.visible: !!hoveredLink - // QQC2.ToolTip.text: googleLoginManager.authorizationCodeUrl - - // ContextMenu - MouseArea { - anchors.fill: parent - acceptedButtons: Qt.RightButton - onClicked: { - if (mouse.button === Qt.RightButton) { - contextMenu.popup() - } - } - onPressAndHold: { - if (mouse.source === Qt.MouseEventNotSynthesized) { - contextMenu.popup() - } - } - - QQC2.Menu { - id: contextMenu - QQC2.MenuItem { - text: i18n("Copy Link") - onTriggered: clipboardHelper.copyText(googleLoginManager.authorizationCodeUrl) - } - } - - TextEdit { - id: clipboardHelper - visible: false - function copyText(text) { - clipboardHelper.text = text - clipboardHelper.selectAll() - clipboardHelper.copy() - } - } - } - } RowLayout { - TextField { - id: authorizationCodeInput - Layout.fillWidth: true - - placeholderText: i18n("Enter code here (Eg: %1)", '1/2B3C4defghijklmnopqrst-uvwxyz123456789ab-cdeFGHIJKlmnio') - text: "" - } Button { - text: i18n("Submit") + text: i18n("Authorize") onClicked: { - if (authorizationCodeInput.text) { - googleLoginManager.fetchAccessToken({ - authorizationCode: authorizationCodeInput.text, - }) - } else { - messageWidget.err(i18n("Invalid Google Authorization Code")) - } + googleLoginManager.fetchAccessToken() } } } diff --git a/package/contents/ui/config/GoogleLoginManager.qml b/package/contents/ui/config/GoogleLoginManager.qml index 2c2cd574..b477bfda 100644 --- a/package/contents/ui/config/GoogleLoginManager.qml +++ b/package/contents/ui/config/GoogleLoginManager.qml @@ -5,6 +5,8 @@ import "../lib/Requests.js" as Requests Item { id: session + ExecUtil { id: executable } + property int callbackListenPort: 8001 Logger { id: logger @@ -72,49 +74,43 @@ Item { signal sessionReset() signal error(string err) - - //--- - readonly property string authorizationCodeUrl: { - var url = 'https://accounts.google.com/o/oauth2/v2/auth' - url += '?scope=' + encodeURIComponent('https://www.googleapis.com/auth/calendar https://www.googleapis.com/auth/tasks') - url += '&response_type=code' - url += '&redirect_uri=' + encodeURIComponent('urn:ietf:wg:oauth:2.0:oob') - url += '&client_id=' + encodeURIComponent(plasmoid.configuration.latestClientId) - return url - } - - function fetchAccessToken(args) { - var url = 'https://www.googleapis.com/oauth2/v4/token' - Requests.post({ - url: url, - data: { - client_id: plasmoid.configuration.latestClientId, - client_secret: plasmoid.configuration.latestClientSecret, - code: args.authorizationCode, - grant_type: 'authorization_code', - redirect_uri: 'urn:ietf:wg:oauth:2.0:oob', - }, - }, function(err, data, xhr) { - logger.debug('/oauth2/v4/token Response', data) - - // Check for errors - if (err) { - handleError(err, null) + readonly property string authorizationCodeUrl: { + var url = 'https://accounts.google.com/o/oauth2/v2/auth' + url += '?scope=' + encodeURIComponent('https://www.googleapis.com/auth/calendar https://www.googleapis.com/auth/tasks') + url += '&response_type=code' + url += '&redirect_uri=' + "http://127.0.0.1:" + callbackListenPort.toString() + "/" + url += '&client_id=' + encodeURIComponent(plasmoid.configuration.latestClientId) + return url + } + + + function fetchAccessToken() { + var cmd = [ + 'python3', + plasmoid.file("", "scripts/google_redirect.py"), + "--client_id", plasmoid.configuration.latestClientId, + "--client_secret", plasmoid.configuration.latestClientSecret, + "--listen_port", callbackListenPort.toString(), + ] + + Qt.openUrlExternally(authorizationCodeUrl); + + executable.exec(cmd, function(cmd, exitCode, exitStatus, stdout, stderr) { + if (exitCode) { + logger.log('fetchAccessToken.stderr', stderr) + logger.log('fetchAccessToken.stdout', stdout) return } + try { - data = JSON.parse(data) + var data = JSON.parse(stdout) + updateAccessToken(data) } catch (e) { - handleError('Error parsing /oauth2/v4/token data as JSON', null) - return - } - if (data && data.error) { - handleError(err, data) + logger.log('fetchAccessToken.e', e) + handleError('Error parsing JSON', null) return } - // Ready - updateAccessToken(data) }) } From 7a7cc2e90d522f405ff2b5e1281ffa056a129439 Mon Sep 17 00:00:00 2001 From: Gaganpreet Arora Date: Thu, 13 Apr 2023 23:36:18 +0200 Subject: [PATCH 2/6] Extract exchange code logic into function --- package/contents/scripts/google_redirect.py | 33 +++++++++++---------- 1 file changed, 18 insertions(+), 15 deletions(-) diff --git a/package/contents/scripts/google_redirect.py b/package/contents/scripts/google_redirect.py index 3282db54..d0ce6a23 100755 --- a/package/contents/scripts/google_redirect.py +++ b/package/contents/scripts/google_redirect.py @@ -11,6 +11,23 @@ client_id = client_secret = listen_port = None +def exchange_code_for_token(code): + # Exchange code for token from https://oauth2.googleapis.com/token + # using the following POST request: + token_params = { + "code": code, + "client_id": client_id, + "client_secret": client_secret, + "redirect_uri": "http://127.0.0.1:{}/".format(listen_port), + "grant_type": "authorization_code", + } + data = urllib.parse.urlencode(token_params).encode("utf-8") + req = urllib.request.Request("https://oauth2.googleapis.com/token", data) + response = urllib.request.urlopen(req) + token_data = json.loads(response.read().decode("utf-8")) + return token_data + + class OAuthRedirectHandler(BaseHTTPRequestHandler): def do_GET(self): query = urlparse(self.path).query @@ -18,28 +35,14 @@ def do_GET(self): # handle OAuth redirect here if "code" in params: code = params["code"][0] - # Exchange code for token from https://oauth2.googleapis.com/token - # using the following POST request: - token_params = { - "code": code, - "client_id": client_id, - "client_secret": client_secret, - "redirect_uri": "http://127.0.0.1:{}/".format(listen_port), - "grant_type": "authorization_code", - } - data = urllib.parse.urlencode(token_params).encode("utf-8") try: - req = urllib.request.Request( - "https://oauth2.googleapis.com/token", data - ) - response = urllib.request.urlopen(req) + token_data = exchange_code_for_token(code) except urllib.error.HTTPError as e: print(e.read().decode("utf-8")) self.wfile.write(b"Handling redirect failed.") raise SystemExit(1) # Parse the response and extract the access token - token_data = json.loads(response.read().decode("utf-8")) with open("/tmp/token.json", "w") as f: json.dump(token_data, f) print(json.dumps(token_data, sort_keys=True)) From 0e0e3ea29f94c6f696736bf399e2f840f59438c4 Mon Sep 17 00:00:00 2001 From: Gaganpreet Arora Date: Thu, 13 Apr 2023 23:39:54 +0200 Subject: [PATCH 3/6] Use tabs --- .../contents/ui/config/GoogleLoginManager.qml | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/package/contents/ui/config/GoogleLoginManager.qml b/package/contents/ui/config/GoogleLoginManager.qml index b477bfda..8f537552 100644 --- a/package/contents/ui/config/GoogleLoginManager.qml +++ b/package/contents/ui/config/GoogleLoginManager.qml @@ -74,26 +74,26 @@ Item { signal sessionReset() signal error(string err) - readonly property string authorizationCodeUrl: { - var url = 'https://accounts.google.com/o/oauth2/v2/auth' - url += '?scope=' + encodeURIComponent('https://www.googleapis.com/auth/calendar https://www.googleapis.com/auth/tasks') - url += '&response_type=code' - url += '&redirect_uri=' + "http://127.0.0.1:" + callbackListenPort.toString() + "/" - url += '&client_id=' + encodeURIComponent(plasmoid.configuration.latestClientId) - return url - } + readonly property string authorizationCodeUrl: { + var url = 'https://accounts.google.com/o/oauth2/v2/auth' + url += '?scope=' + encodeURIComponent('https://www.googleapis.com/auth/calendar https://www.googleapis.com/auth/tasks') + url += '&response_type=code' + url += '&redirect_uri=' + "http://127.0.0.1:" + callbackListenPort.toString() + "/" + url += '&client_id=' + encodeURIComponent(plasmoid.configuration.latestClientId) + return url + } function fetchAccessToken() { var cmd = [ 'python3', plasmoid.file("", "scripts/google_redirect.py"), - "--client_id", plasmoid.configuration.latestClientId, - "--client_secret", plasmoid.configuration.latestClientSecret, - "--listen_port", callbackListenPort.toString(), + "--client_id", plasmoid.configuration.latestClientId, + "--client_secret", plasmoid.configuration.latestClientSecret, + "--listen_port", callbackListenPort.toString(), ] - Qt.openUrlExternally(authorizationCodeUrl); + Qt.openUrlExternally(authorizationCodeUrl); executable.exec(cmd, function(cmd, exitCode, exitStatus, stdout, stderr) { if (exitCode) { From 22844ef613d2081fb1ed4d0c64307e040e4e9e91 Mon Sep 17 00:00:00 2001 From: Gaganpreet Arora Date: Thu, 13 Apr 2023 23:42:59 +0200 Subject: [PATCH 4/6] Remove debug output --- package/contents/scripts/google_redirect.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/package/contents/scripts/google_redirect.py b/package/contents/scripts/google_redirect.py index d0ce6a23..4bdd46b8 100755 --- a/package/contents/scripts/google_redirect.py +++ b/package/contents/scripts/google_redirect.py @@ -41,10 +41,6 @@ def do_GET(self): print(e.read().decode("utf-8")) self.wfile.write(b"Handling redirect failed.") raise SystemExit(1) - - # Parse the response and extract the access token - with open("/tmp/token.json", "w") as f: - json.dump(token_data, f) print(json.dumps(token_data, sort_keys=True)) self.send_response(200) From aadf4b441750f186fe1923e3fc3b41c0052b6415 Mon Sep 17 00:00:00 2001 From: Gaganpreet Arora Date: Thu, 13 Apr 2023 23:50:12 +0200 Subject: [PATCH 5/6] handle scenario when code param is missing --- package/contents/scripts/google_redirect.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/package/contents/scripts/google_redirect.py b/package/contents/scripts/google_redirect.py index 4bdd46b8..bae0d6f0 100755 --- a/package/contents/scripts/google_redirect.py +++ b/package/contents/scripts/google_redirect.py @@ -43,14 +43,15 @@ def do_GET(self): raise SystemExit(1) print(json.dumps(token_data, sort_keys=True)) - self.send_response(200) - self.send_header("Content-type", "text/html") - self.end_headers() - self.wfile.write( - b"OAuth redirect handled successfully. You can close this tab now." - ) - # Exit the server - raise SystemExit(0) + self.send_response(200) + self.send_header("Content-type", "text/html") + self.end_headers() + self.wfile.write( + b"OAuth redirect handled successfully. You can close this tab now." + ) + raise SystemExit(0) + self.wfile.write(b"Missing code parameter in redirect.") + raise SystemExit(1) if __name__ == "__main__": From bdad0b77657550759e76553f79b837aac8edc5a7 Mon Sep 17 00:00:00 2001 From: Gaganpreet Arora Date: Wed, 19 Apr 2023 23:26:35 +0200 Subject: [PATCH 6/6] Use encodeURIComponent for param --- package/contents/ui/config/GoogleLoginManager.qml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package/contents/ui/config/GoogleLoginManager.qml b/package/contents/ui/config/GoogleLoginManager.qml index 8f537552..728a5de7 100644 --- a/package/contents/ui/config/GoogleLoginManager.qml +++ b/package/contents/ui/config/GoogleLoginManager.qml @@ -78,7 +78,7 @@ Item { var url = 'https://accounts.google.com/o/oauth2/v2/auth' url += '?scope=' + encodeURIComponent('https://www.googleapis.com/auth/calendar https://www.googleapis.com/auth/tasks') url += '&response_type=code' - url += '&redirect_uri=' + "http://127.0.0.1:" + callbackListenPort.toString() + "/" + url += '&redirect_uri=' + encodeURIComponent("http://127.0.0.1:" + callbackListenPort.toString() + "/") url += '&client_id=' + encodeURIComponent(plasmoid.configuration.latestClientId) return url }