[gnome-maps/wip/mlundblad/osm-oauth-external] WIP: Use external browser to authorize OAuth token
- From: Marcus Lundblad <mlundblad src gnome org>
- To: commits-list gnome org
- Cc:
- Subject: [gnome-maps/wip/mlundblad/osm-oauth-external] WIP: Use external browser to authorize OAuth token
- Date: Mon, 25 Apr 2022 20:56:35 +0000 (UTC)
commit 35465564bcf19a9fe4cc361d66108d3b58e688c5
Author: Marcus Lundblad <ml dfupdate se>
Date: Sat Apr 23 23:05:34 2022 +0200
WIP: Use external browser to authorize OAuth token
data/ui/osm-account-dialog.ui | 148 +++++++++---------------------------
src/osmAccountDialog.js | 94 +++++++----------------
src/osmConnection.js | 172 +++++++++++-------------------------------
src/osmEdit.js | 35 ++++-----
4 files changed, 120 insertions(+), 329 deletions(-)
---
diff --git a/data/ui/osm-account-dialog.ui b/data/ui/osm-account-dialog.ui
index 8cddfeca..ded47b7e 100644
--- a/data/ui/osm-account-dialog.ui
+++ b/data/ui/osm-account-dialog.ui
@@ -12,6 +12,7 @@
<child>
<object class="GtkStack" id="stack">
<property name="visible">True</property>
+ <property name="transition-type">GTK_STACK_TRANSITION_TYPE_SLIDE_RIGHT</property>
<child>
<object class="GtkGrid">
<property name="visible">True</property>
@@ -50,59 +51,6 @@ OpenStreetMap account.</property>
<property name="visible">True</property>
<property name="column-spacing">10</property>
<property name="row-spacing">10</property>
- <child>
- <object class="GtkLabel">
- <property name="visible">True</property>
- <property name="label" translatable="yes">Email</property>
- <property name="halign">GTK_ALIGN_END</property>
- <style>
- <class name="dim-label"/>
- </style>
- </object>
- <packing>
- <property name="left_attach">0</property>
- <property name="top_attach">0</property>
- </packing>
- </child>
- <child>
- <object class="GtkEntry" id="emailEntry">
- <property name="visible">True</property>
- <property name="hexpand">True</property>
- </object>
- <packing>
- <property name="left_attach">1</property>
- <property name="top_attach">0</property>
- <property name="width">2</property>
- </packing>
- </child>
- <child>
- <object class="GtkLabel">
- <property name="visible">True</property>
- <property name="label" translatable="yes">Password</property>
- <property name="halign">GTK_ALIGN_END</property>
- <style>
- <class name="dim-label"/>
- </style>
- </object>
- <packing>
- <property name="left_attach">0</property>
- <property name="top_attach">1</property>
- </packing>
- </child>
- <child>
- <object class="GtkEntry" id="passwordEntry">
- <property name="visible">True</property>
- <property name="hexpand">True</property>
- <property name="input-purpose">GTK_INPUT_PURPOSE_PASSWORD</property>
- <property name="visibility">False</property>
- <property name="caps-lock-warning">True</property>
- </object>
- <packing>
- <property name="left_attach">1</property>
- <property name="top_attach">1</property>
- <property name="width">2</property>
- </packing>
- </child>
<child>
<object class="GtkSpinner" id="signInSpinner">
<property name="visible">False</property>
@@ -119,7 +67,7 @@ OpenStreetMap account.</property>
</packing>
</child>
<child>
- <object class="GtkLinkButton" id="signUpLinkButton">
+ <object class="GtkLinkButton">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="label" translatable="yes">Sign up</property>
@@ -137,7 +85,6 @@ OpenStreetMap account.</property>
<property name="visible">True</property>
<property name="halign">GTK_ALIGN_END</property>
<property name="label" translatable="yes">Sign In</property>
- <property name="sensitive">False</property>
<style>
<class name="suggested-action"/>
</style>
@@ -153,26 +100,10 @@ OpenStreetMap account.</property>
<property name="top_attach">2</property>
</packing>
</child>
-
<child>
- <object class="GtkLabel" id="resetPasswordLabel">
+ <object class="GtkLabel" id="errorLabel">
<property name="visible">False</property>
<property name="can_focus">True</property>
- <property name="label" translatable="yes"
- comments="The label should contain the link to the OSM reset password page with a
translated title">Sorry, that didn’t work. Please try again, or visit
-<a href="https://www.openstreetmap.org/user/forgot-password">OpenStreetMap</a> to reset your
password.</property>
- <property name="use-markup">True</property>
- </object>
- <packing>
- <property name="left_attach">0</property>
- <property name="top_attach">4</property>
- </packing>
- </child>
- <child>
- <object class="GtkLabel" id="verificationFailedLabel">
- <property name="visible">False</property>
- <property name="can_focus">True</property>
- <property name="label" translatable="yes">The verification code didn’t match, please try
again.</property>
<property name="use-markup">True</property>
</object>
<packing>
@@ -186,56 +117,45 @@ OpenStreetMap account.</property>
</packing>
</child>
<child>
- <object class="GtkGrid" id="verifyGrid">
+ <object class="GtkGrid">
<property name="visible">True</property>
<property name="row-spacing">10</property>
<property name="margin">20</property>
<child>
- <object class="GtkGrid">
+ <object class="GtkLabel">
<property name="visible">True</property>
- <property name="column-spacing">10</property>
- <property name="row-spacing">10</property>
- <child>
- <object class="GtkLabel">
- <property name="visible">True</property>
- <property name="label" translatable="yes">Enter verification code shown
above</property>
- <style>
- <class name="dim-label"/>
- </style>
- </object>
- <packing>
- <property name="left_attach">0</property>
- <property name="top_attach">0</property>
- </packing>
- </child>
- <child>
- <object class="GtkEntry" id="verificationEntry">
- <property name="visible">True</property>
- </object>
- <packing>
- <property name="left_attach">1</property>
- <property name="top_attach">0</property>
- </packing>
- </child>
- <child>
- <object class="GtkButton" id="verifyButton">
- <property name="visible">True</property>
- <property name="sensitive">False</property>
- <property name="label" translatable="yes">Verify</property>
- <property name="hexpand">False</property>
- <property name="halign">GTK_ALIGN_END</property>
- <style>
- <class name="suggested-action"/>
- </style>
- </object>
- <packing>
- <property name="left_attach">1</property>
- <property name="top_attach">1</property>
- </packing>
- </child>
+ <property name="label" translatable="yes">Enter verification code shown above</property>
+ <style>
+ <class name="dim-label"/>
+ </style>
</object>
<packing>
<property name="left_attach">0</property>
+ <property name="top_attach">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkEntry" id="verificationEntry">
+ <property name="visible">True</property>
+ </object>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="top_attach">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkButton" id="verifyButton">
+ <property name="visible">True</property>
+ <property name="sensitive">False</property>
+ <property name="label" translatable="yes">Verify</property>
+ <property name="hexpand">False</property>
+ <property name="halign">GTK_ALIGN_END</property>
+ <style>
+ <class name="suggested-action"/>
+ </style>
+ </object>
+ <packing>
+ <property name="left_attach">1</property>
<property name="top_attach">1</property>
</packing>
</child>
diff --git a/src/osmAccountDialog.js b/src/osmAccountDialog.js
index d522e456..21c63fe3 100644
--- a/src/osmAccountDialog.js
+++ b/src/osmAccountDialog.js
@@ -22,7 +22,6 @@
const GObject = imports.gi.GObject;
const Gtk = imports.gi.Gtk;
-const WebKit2 = imports.gi.WebKit2;
const Application = imports.application;
const Utils = imports.utils;
@@ -34,16 +33,11 @@ var Response = {
var OSMAccountDialog = GObject.registerClass({
Template: 'resource:///org/gnome/Maps/ui/osm-account-dialog.ui',
InternalChildren: ['stack',
- 'emailEntry',
- 'passwordEntry',
'signInButton',
'signInSpinner',
- 'signUpLinkButton',
- 'resetPasswordLabel',
- 'verifyGrid',
'verificationEntry',
'verifyButton',
- 'verificationFailedLabel',
+ 'errorLabel',
'signedInUserLabel',
'signOutButton'],
}, class OSMAccountDialog extends Gtk.Dialog {
@@ -57,12 +51,6 @@ var OSMAccountDialog = GObject.registerClass({
super._init(params);
- this._emailEntry.connect('changed',
- this._onCredentialsChanged.bind(this));
- this._passwordEntry.connect('changed',
- this._onCredentialsChanged.bind(this));
- this._passwordEntry.connect('activate',
- this._onPasswordActivated.bind(this));
this._signInButton.connect('clicked',
this._onSignInButtonClicked.bind(this));
this._verifyButton.connect('clicked',
@@ -76,76 +64,47 @@ var OSMAccountDialog = GObject.registerClass({
/* if the user is logged in, show the logged-in view */
if (Application.osmEdit.isSignedIn) {
- this._signedInUserLabel.label = Application.osmEdit.username;
+ this._updateSignedInUserLabel();
this._stack.visible_child_name = 'logged-in';
}
-
- /* initialize verification web view, we do it programmatically rather
- * declare it in the .ui file to be able to enable WebKit sandboxing
- */
- let webContext = WebKit2.WebContext.get_default();
-
- webContext.set_sandbox_enabled(true);
- this._verifyView = WebKit2.WebView.new_with_context(webContext);
- this._verifyView.visible = true;
- this._verifyView.halign = Gtk.Align.FILL;
- this._verifyView.height_request = 150;
- this._verifyGrid.attach(this._verifyView, 0, 0, 1, 1);
}
- _onCredentialsChanged() {
- let email = this._emailEntry.text;
- let password = this._passwordEntry.text;
-
- // make sign in button sensitive if credential have been entered
- this._signInButton.sensitive =
- email && email.length > 0 && password && password.length > 0;
+ _updateSignedInUserLabel() {
+ /* if we couldn't determine the logged in username (e.g. the user
+ * didn't grant permission to read user details, hide the username
+ * label
+ */
+ if (Application.osmEdit.username === '_unknown_') {
+ this._signedInUserLabel.visible = false;
+ } else {
+ this._signedInUserLabel.label = Application.osmEdit.username;
+ this._signedInUserLabel.visible = true;
+ }
}
_onSignInButtonClicked() {
this._performSignIn();
}
- _onPasswordActivated() {
- /* if username and password was entered, proceed with sign-in */
- let email = this._emailEntry.text;
- let password = this._passwordEntry.text;
-
- if (email && email.length > 0 && password && password.length > 0)
- this._performSignIn();
- }
-
_performSignIn() {
- /* turn on signing in spinner and desensisize credential entries */
+ // turn on signing in spinner
this._signInSpinner.visible = true;
this._signInButton.sensitive = false;
- this._emailEntry.sensitive = false;
- this._passwordEntry.sensitive = false;
- this._signUpLinkButton.visible = false;
- Application.osmEdit.performOAuthSignIn(this._emailEntry.text,
- this._passwordEntry.text,
- this._onOAuthSignInPerformed.bind(this));
+ Application.osmEdit.performOAuthSignIn(this._onOAuthTokenAuthorized.bind(this));
}
- _onOAuthSignInPerformed(success, verificationPage) {
+ _onOAuthTokenAuthorized(success) {
if (success) {
- /* switch to the verification view and show the verification
- page */
- this._verifyView.load_html(verificationPage,
- 'https://www.openstreetmap.org/');
+ // switch to the verification view
this._stack.visible_child_name = 'verify';
} else {
- /* clear password entry */
- this._passwordEntry.text = '';
- /* show the password reset link */
- this._resetPasswordLabel.visible = true;
+ this._errorLabel.visible = true;
+ this._errorLabel.label = _("Failed to authorize access");
+ this._signInButton.label = _("Try again");
}
this._signInSpinner.visible = false;
- /* re-sensisize credential entries */
- this._emailEntry.sensitive = true;
- this._passwordEntry.sensitive = true;
}
_onVerifyButtonClicked() {
@@ -185,16 +144,14 @@ var OSMAccountDialog = GObject.registerClass({
_onOAuthAccessTokenRequested(success, errorMessage) {
if (success) {
/* update the username label */
- this._signedInUserLabel.label = Application.osmEdit.username;
+ this._updateSignedInUserLabel();
if (this._closeOnSignIn) {
this.response(Response.SIGNED_IN);
} else {
/* switch to the logged in view and reset the state in case
the user signs out and start over again */
- this._resetPasswordLabel.visible = false;
- this._verificationFailedLabel = false;
- this._signUpLinkButton.visible = true;
+ this._errorLabel.visible = false;
this._stack.visible_child_name = 'logged-in';
}
} else {
@@ -202,9 +159,9 @@ var OSMAccountDialog = GObject.registerClass({
Utils.showDialog(errorMessage, Gtk.MessageType.ERROR, this);
/* switch back to the sign-in view, and show a label indicating
that verification failed */
- this._resetPasswordLabel.visible = false;
- this._signUpLinkButton.visible = false;
- this._verificationFailedLabel.visible = true;
+ this._errorLabel.visible = true;
+ this._errorLabel.label =
+ _("The verification code didn’t match, please try again.");
this._signInButton.sensitive = true;
this._stack.visible_child_name = 'sign-in';
}
@@ -214,6 +171,7 @@ var OSMAccountDialog = GObject.registerClass({
_onSignOutButtonClicked() {
Application.osmEdit.signOut();
+ this._signInButton.sensitive= true;
this._stack.visible_child_name = 'sign-in';
}
});
diff --git a/src/osmConnection.js b/src/osmConnection.js
index 6bb7026c..f20e8600 100644
--- a/src/osmConnection.js
+++ b/src/osmConnection.js
@@ -23,6 +23,8 @@
const _ = imports.gettext.gettext;
const Maps = imports.gi.GnomeMaps;
+
+const Gio = imports.gi.Gio;
const Rest = imports.gi.Rest;
const Secret = imports.gi.Secret;
const Soup = imports.gi.Soup;
@@ -238,102 +240,19 @@ var OSMConnection = class OSMConnection {
callback(true);
}
- authorizeOAuthToken(username, password, callback) {
- /* get login session ID */
- let loginUrl = LOGIN_URL + '?cookie_test=true';
- let uri = new Soup.URI(loginUrl);
- let msg = new Soup.Message({method: 'GET', uri: uri});
+ authorizeOAuthToken(callback) {
+ let auth = '/authorize?oauth_token=';
+ let authorizeUrl = OAUTH_ENDPOINT_URL + auth + this._oauthToken;
- this._session.queue_message(msg, (obj, message) => {
- this._onLoginFormReceived(message, username, password, callback);
- });
- }
+ Utils.debug('Trying to open: ' + authorizeUrl);
- _onLoginFormReceived(message, username, password, callback) {
- if (message.status_code !== Soup.Status.OK) {
+ try {
+ Gio.AppInfo.launch_default_for_uri(authorizeUrl, null);
+ callback(true);
+ } catch (e) {
+ Utils.debug('error: ' + e.message);
callback(false);
- return;
}
-
- let osmSessionID =
- this._extractOSMSessionID(message.response_headers);
- let osmSessionToken =
- this._extractToken(message.response_body.data);
-
- if (osmSessionID === null || osmSessionToken === null) {
- callback(false, null);
- return;
- }
-
- this._login(username, password, osmSessionID, osmSessionToken, callback);
- }
-
- _login(username, password, sessionId, token, callback) {
- /* post login form */
- let msg = Soup.form_request_new_from_hash('POST', LOGIN_URL,
- {username: username,
- password: password,
- referer: '/',
- commit: 'Login',
- authenticity_token: token});
- let requestHeaders = msg.request_headers;
-
- requestHeaders.append('Content-Type',
- 'application/x-www-form-urlencoded');
- requestHeaders.append('Cookie', '_osm_session=' + sessionId);
- msg.flags |= Soup.MessageFlags.NO_REDIRECT;
-
- this._session.queue_message(msg, (obj, message) => {
- if (message.status_code === Soup.Status.MOVED_TEMPORARILY)
- this._fetchAuthorizeForm(username, sessionId, callback);
- else
- callback(false, null);
- });
-
- }
-
- _fetchAuthorizeForm(username, sessionId, callback) {
- let auth = '/authorize?oauth_token=';
- let authorizeUrl = OAUTH_ENDPOINT_URL + auth + this._oauthToken;
- let uri = new Soup.URI(authorizeUrl);
- let msg = new Soup.Message({uri: uri, method: 'GET'});
-
- msg.request_headers.append('Cookie',
- '_osm_session=' + sessionId +
- '; _osm_username=' + username);
- this._session.queue_message(msg, (obj, message) => {
- if (message.status_code === Soup.Status.OK) {
- let token = this._extractToken(message.response_body.data);
- this._postAuthorizeForm(username, sessionId, token, callback);
- } else {
- callback(false, null);
- }
- });
- }
-
- _postAuthorizeForm(username, sessionId, token, callback) {
- let authorizeUrl = OAUTH_ENDPOINT_URL + '/authorize';
- let msg = Soup.form_request_new_from_hash('POST', authorizeUrl, {
- oauth_token: this._oauthToken,
- oauth_callback: '',
- authenticity_token: token,
- allow_write_api: '1',
- commit: 'Save changes'
- });
- let requestHeaders = msg.request_headers;
-
- requestHeaders.append('Content-Type',
- 'application/x-www-form-urlencoded');
- requestHeaders.append('Cookie',
- '_osm_session=' + sessionId +
- '; _osm_username=' + username);
-
- this._session.queue_message(msg, (obj, message) => {
- if (msg.status_code === Soup.Status.OK) {
- callback(true, message.response_body.data);
- } else
- callback(false, null);
- });
}
requestOAuthAccessToken(code, callback) {
@@ -342,6 +261,37 @@ var OSMConnection = class OSMConnection {
}, this._oauthProxy);
}
+ fetchLoggedInUser(callback) {
+ let call = this._callProxy.new_call();
+ call.set_method('GET');
+ call.set_function('/user/details');
+
+ call.invoke_async(null, (call, res, userdata) =>
+ { this._onFetchedLoggedInUser(call, callback); });
+ }
+
+ _onFetchedLoggedInUser(call, callback) {
+ switch (call.get_status_code()) {
+ case Soup.Status.OK:
+ try {
+ callback(Maps.osm_parse_user_details(call.get_payload()));
+ } catch (e) {
+ Utils.debug('Error parsing user details: ' + e.message);
+ callback(null);
+ }
+ break;
+ default:
+ /* Not ok, most likely 403 (forbidden), meaning the user
+ * didn't give permission to read user details.
+ * Just consider the user name unknown in this case
+ */
+ Utils.debug('Got status code ' + call.get_status_code() +
+ ' getting user details');
+ callback(null);
+ break;
+ }
+ }
+
_onAccessOAuthToken(error, callback) {
if (error) {
callback(false);
@@ -389,44 +339,6 @@ var OSMConnection = class OSMConnection {
_onPasswordCleared(source, result) {
Secret.password_clear_finish(result);
}
-
- /* extract the session ID from the login form response headers */
- _extractOSMSessionID(responseHeaders) {
- let cookie = responseHeaders.get('Set-Cookie');
-
- if (cookie === null)
- return null;
-
- let cookieParts = cookie.split(';');
- for (let cookiePart of cookieParts) {
- let kvPair = cookiePart.trim().split('=');
- let kv = kvPair;
-
- if (kv.length !== 2) {
- continue;
- } else if (kv[0] === '_osm_session') {
- return kv[1];
- }
- }
-
- return null;
- }
-
- /* extract the authenticity token from the hidden input field of the login
- form */
- _extractToken(messageBody) {
- let regex = /.*authenticity_token.*value=\"([^\"]+)\".*/;
- let lines = messageBody.split('\n');
-
- for (let line of lines) {
- let match = line.match(regex);
-
- if (match && match.length === 2)
- return match[1];
- }
-
- return null;
- }
};
/*
diff --git a/src/osmEdit.js b/src/osmEdit.js
index fa1a7e5d..bd3d6256 100644
--- a/src/osmEdit.js
+++ b/src/osmEdit.js
@@ -142,21 +142,16 @@ var OSMEdit = class OSMEdit {
this._osmConnection.closeChangeset(changesetId, callback);
}
- performOAuthSignIn(username, password, callback) {
+ performOAuthSignIn(callback) {
this._osmConnection.requestOAuthToken((success) => {
- if (success)
- this._onOAuthTokenRequested(username, password, callback);
- else
- callback(false, null);
+ if (success) {
+ this._osmConnection.authorizeOAuthToken(callback);
+ } else {
+ callback(false);
+ }
});
}
- _onOAuthTokenRequested(username, password, callback) {
- /* keep track of authorizing username */
- this._username = username;
- this._osmConnection.authorizeOAuthToken(username, password, callback);
- }
-
requestOAuthAccessToken(code, callback) {
this._osmConnection.requestOAuthAccessToken(code, (success, token) => {
this._onOAuthAccessTokenRequested(success, callback);
@@ -165,14 +160,20 @@ var OSMEdit = class OSMEdit {
_onOAuthAccessTokenRequested(success, callback) {
if (success) {
- this._isSignedIn = true;
- Application.settings.set('osm-username', this._username);
+ this._osmConnection.fetchLoggedInUser((username) => {
+ this._isSignedIn = true;
+ /* if we couldn't retrieve the logged-in username,
+ * e.g. if the user de-selected the permission when
+ * authorizing the OAuth token, use a dummy placeholder
+ * username to signify that we are signed in
+ */
+ this._username = username ?? '_unknown_';
+ Application.settings.set('osm-username', this._username);
+ callback(true);
+ });
} else {
- /* clear out username if verification was unsuccessful */
- this._username = null;
+ callback(false);
}
-
- callback(success);
}
signOut() {
[
Date Prev][
Date Next] [
Thread Prev][
Thread Next]
[
Thread Index]
[
Date Index]
[
Author Index]