Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Google oauth handler using an external Python script #345

Open
wants to merge 6 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 69 additions & 0 deletions package/contents/scripts/google_redirect.py
Original file line number Diff line number Diff line change
@@ -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


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
params = parse_qs(query)
# handle OAuth redirect here
if "code" in params:
code = params["code"][0]
try:
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)
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."
)
raise SystemExit(0)
self.wfile.write(b"Missing code parameter in redirect.")
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is the path of this file? Or is it stdout? or stderr? Is there a reason for using write instead of print() (no newline)?

https://docs.python.org/3/library/http.server.html#http.server.BaseHTTPRequestHandler.wfile

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

self.wfile is to write the response back to the client, in this case the browser (to guide the user what to do). For example, after a successful redirect, the user will see this in their browser:

Screenshot_20230419_232904

The print output is for communication with the QML script (using the executable.exec helper).

raise SystemExit(1)


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()
63 changes: 3 additions & 60 deletions package/contents/ui/config/ConfigGoogleCalendar.qml
Original file line number Diff line number Diff line change
Expand Up @@ -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 <a href=\"%1\">%2</a> (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()
}
}
}
Expand Down
50 changes: 23 additions & 27 deletions package/contents/ui/config/GoogleLoginManager.qml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import "../lib/Requests.js" as Requests

Item {
id: session
ExecUtil { id: executable }
property int callbackListenPort: 8001

Logger {
id: logger
Expand Down Expand Up @@ -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 += '&redirect_uri=' + encodeURIComponent("http://127.0.0.1:" + callbackListenPort.toString() + "/")
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)
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)
})
}

Expand Down