[gnome-maps/wip/mlundblad/osm-oauth-external] WIP: Use external browser to authorize OAuth token




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
-&lt;a href="https://www.openstreetmap.org/user/forgot-password"&gt;OpenStreetMap&lt;/a&gt; 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]