diff options
Diffstat (limited to 'plugins/authenticator/shotwell/GoogleAuthenticator.vala')
-rw-r--r-- | plugins/authenticator/shotwell/GoogleAuthenticator.vala | 219 |
1 files changed, 109 insertions, 110 deletions
diff --git a/plugins/authenticator/shotwell/GoogleAuthenticator.vala b/plugins/authenticator/shotwell/GoogleAuthenticator.vala index a607cd0..3276e67 100644 --- a/plugins/authenticator/shotwell/GoogleAuthenticator.vala +++ b/plugins/authenticator/shotwell/GoogleAuthenticator.vala @@ -7,6 +7,9 @@ namespace Publishing.Authenticator.Shotwell.Google { private const string OAUTH_CLIENT_SECRET = "pwpzZ7W1TCcD5uIfYCu8sM7x"; private const string OAUTH_CALLBACK_URI = REVERSE_CLIENT_ID + ":/auth-callback"; + private const string SCHEMA_KEY_PROFILE_ID = "shotwell-profile-id"; + private const string SCHEMA_KEY_ACCOUNTNAME = "accountname"; + private class WebAuthenticationPane : Common.WebAuthenticationPane { public static bool cache_dirty = false; private string? auth_code = null; @@ -27,10 +30,15 @@ namespace Publishing.Authenticator.Shotwell.Google { return; } - var uri = new Soup.URI(get_view().get_uri()); - if (uri.scheme == REVERSE_CLIENT_ID && this.auth_code == null) { - var form_data = Soup.Form.decode (uri.query); - this.auth_code = form_data.lookup("code"); + try { + var uri = GLib.Uri.parse(get_view().get_uri(), UriFlags.NONE); + if (uri.get_scheme() == REVERSE_CLIENT_ID && this.auth_code == null) { + var form_data = Soup.Form.decode (uri.get_query()); + this.auth_code = form_data.lookup("code"); + } + } catch (Error err) { + debug ("Failed to parse auth code from URI %s: %s", get_view().get_uri(), + err.message); } if (this.auth_code != null) { @@ -39,10 +47,14 @@ namespace Publishing.Authenticator.Shotwell.Google { } private void on_shotwell_auth_request_cb(WebKit.URISchemeRequest request) { - var uri = new Soup.URI(request.get_uri()); - debug("URI: %s", request.get_uri()); - var form_data = Soup.Form.decode (uri.query); - this.auth_code = form_data.lookup("code"); + try { + var uri = GLib.Uri.parse(request.get_uri(), GLib.UriFlags.NONE); + debug("URI: %s", request.get_uri()); + var form_data = Soup.Form.decode (uri.get_query()); + this.auth_code = form_data.lookup("code"); + } catch (Error err) { + debug("Failed to parse request URI: %s", err.message); + } var response = ""; var mins = new MemoryInputStream.from_data(response.data, null); @@ -77,7 +89,7 @@ namespace Publishing.Authenticator.Shotwell.Google { } private class GetAccessTokensTransaction : Publishing.RESTSupport.Transaction { - private const string ENDPOINT_URL = "https://accounts.google.com/o/oauth2/token"; + private const string ENDPOINT_URL = "https://oauth2.googleapis.com/token"; public GetAccessTokensTransaction(Session session, string auth_code) { base.with_endpoint_url(session, ENDPOINT_URL); @@ -91,7 +103,7 @@ namespace Publishing.Authenticator.Shotwell.Google { } private class RefreshAccessTokenTransaction : Publishing.RESTSupport.Transaction { - private const string ENDPOINT_URL = "https://accounts.google.com/o/oauth2/token"; + private const string ENDPOINT_URL = "https://oauth2.googleapis.com/token"; public RefreshAccessTokenTransaction(Session session) { base.with_endpoint_url(session, ENDPOINT_URL); @@ -112,12 +124,18 @@ namespace Publishing.Authenticator.Shotwell.Google { } internal class Google : Spit.Publishing.Authenticator, Object { + private const string PASSWORD_SCHEME = "org.gnome.Shotwell.Google"; + private string scope = null; + + // Prepare for multiple user accounts + private string accountname = "default"; private Spit.Publishing.PluginHost host = null; private GLib.HashTable<string, Variant> params = null; private WebAuthenticationPane web_auth_pane = null; private Session session = null; private string welcome_message = null; + private Secret.Schema? schema = null; public Google(string scope, string welcome_message, @@ -127,13 +145,24 @@ namespace Publishing.Authenticator.Shotwell.Google { this.scope = scope; this.session = new Session(); this.welcome_message = welcome_message; + this.schema = new Secret.Schema(PASSWORD_SCHEME, Secret.SchemaFlags.NONE, + SCHEMA_KEY_PROFILE_ID, Secret.SchemaAttributeType.STRING, + SCHEMA_KEY_ACCOUNTNAME, Secret.SchemaAttributeType.STRING, + "scope", Secret.SchemaAttributeType.STRING); } public void authenticate() { - var refresh_token = host.get_config_string("refresh_token", null); + string? refresh_token = null; + try { + refresh_token = Secret.password_lookup_sync(this.schema, null, + SCHEMA_KEY_PROFILE_ID, host.get_current_profile_id(), + SCHEMA_KEY_ACCOUNTNAME, this.accountname, "scope", this.scope); + } catch (Error err) { + critical("Failed to lookup refresh_token from password store: %s", err.message); + } if (refresh_token != null && refresh_token != "") { on_refresh_token_available(refresh_token); - do_exchange_refresh_token_for_access_token(); + do_exchange_refresh_token_for_access_token.begin(); return; } @@ -157,22 +186,32 @@ namespace Publishing.Authenticator.Shotwell.Google { public void logout() { session.deauthenticate(); - host.set_config_string("refresh_token", ""); + try { + Secret.password_clear_sync(this.schema, null, + SCHEMA_KEY_PROFILE_ID, host.get_current_profile_id(), + SCHEMA_KEY_ACCOUNTNAME, this.accountname, "scope", this.scope); + } catch (Error err) { + critical("Failed to remove password for scope %s: %s", this.scope, err.message); + } } public void refresh() { // TODO: Needs to re-auth } + public void set_accountname(string accountname) { + this.accountname = accountname; + } + private void do_hosted_web_authentication() { debug("ACTION: running OAuth authentication flow in hosted web pane."); string user_authorization_url = "https://accounts.google.com/o/oauth2/auth?" + "response_type=code&" + "client_id=" + OAUTH_CLIENT_ID + "&" + - "redirect_uri=" + Soup.URI.encode(OAUTH_CALLBACK_URI, null) + "&" + - "scope=" + Soup.URI.encode(this.scope, null) + "+" + - Soup.URI.encode("https://www.googleapis.com/auth/userinfo.profile", null) + "&" + + "redirect_uri=" + GLib.Uri.escape_string(OAUTH_CALLBACK_URI, null) + "&" + + "scope=" + GLib.Uri.escape_string(this.scope, null) + "+" + + GLib.Uri.escape_string("https://www.googleapis.com/auth/userinfo.profile", null) + "&" + "state=connect&" + "access_type=offline&" + "approval_prompt=force"; @@ -189,48 +228,31 @@ namespace Publishing.Authenticator.Shotwell.Google { debug("EVENT: user authorized scope %s with auth_code %s", scope, auth_code); - do_get_access_tokens(auth_code); + do_get_access_tokens.begin(auth_code); } private void on_web_auth_pane_error() { host.post_error(web_auth_pane.load_error); } - private void do_get_access_tokens(string auth_code) { + private async void do_get_access_tokens(string auth_code) { debug("ACTION: exchanging authorization code for access & refresh tokens"); host.install_login_wait_pane(); GetAccessTokensTransaction tokens_txn = new GetAccessTokensTransaction(session, auth_code); - tokens_txn.completed.connect(on_get_access_tokens_complete); - tokens_txn.network_error.connect(on_get_access_tokens_error); try { - tokens_txn.execute(); - } catch (Spit.Publishing.PublishingError err) { + yield tokens_txn.execute_async(); + debug("EVENT: network transaction to exchange authorization code for access tokens " + + "completed successfully."); + do_extract_tokens(tokens_txn.get_response()); + } catch (Error err) { + debug("EVENT: network transaction to exchange authorization code for access tokens " + + "failed; response = '%s'", tokens_txn.get_response()); host.post_error(err); } - } - - private void on_get_access_tokens_complete(Publishing.RESTSupport.Transaction txn) { - txn.completed.disconnect(on_get_access_tokens_complete); - txn.network_error.disconnect(on_get_access_tokens_error); - debug("EVENT: network transaction to exchange authorization code for access tokens " + - "completed successfully."); - - do_extract_tokens(txn.get_response()); - } - - private void on_get_access_tokens_error(Publishing.RESTSupport.Transaction txn, - Spit.Publishing.PublishingError err) { - txn.completed.disconnect(on_get_access_tokens_complete); - txn.network_error.disconnect(on_get_access_tokens_error); - - debug("EVENT: network transaction to exchange authorization code for access tokens " + - "failed; response = '%s'", txn.get_response()); - - host.post_error(err); } private void do_extract_tokens(string response_body) { @@ -297,45 +319,28 @@ namespace Publishing.Authenticator.Shotwell.Google { session.access_token = token; this.params.insert("AccessToken", new Variant.string(token)); - do_fetch_username(); + do_fetch_username.begin(); } - private void do_fetch_username() { + private async void do_fetch_username() { debug("ACTION: running network transaction to fetch username."); host.install_login_wait_pane(); host.set_service_locked(true); UsernameFetchTransaction txn = new UsernameFetchTransaction(session); - txn.completed.connect(on_fetch_username_transaction_completed); - txn.network_error.connect(on_fetch_username_transaction_error); try { - txn.execute(); + yield txn.execute_async(); + debug("EVENT: username fetch transaction completed successfully."); + do_extract_username(txn.get_response()); } catch (Error err) { + debug("EVENT: username fetch transaction caused a network error"); + host.post_error(err); } } - private void on_fetch_username_transaction_completed(Publishing.RESTSupport.Transaction txn) { - txn.completed.disconnect(on_fetch_username_transaction_completed); - txn.network_error.disconnect(on_fetch_username_transaction_error); - - debug("EVENT: username fetch transaction completed successfully."); - - do_extract_username(txn.get_response()); - } - - private void on_fetch_username_transaction_error(Publishing.RESTSupport.Transaction txn, - Spit.Publishing.PublishingError err) { - txn.completed.disconnect(on_fetch_username_transaction_completed); - txn.network_error.disconnect(on_fetch_username_transaction_error); - - debug("EVENT: username fetch transaction caused a network error"); - - host.post_error(err); - } - private void do_extract_username(string response_body) { debug("ACTION: extracting username from body of server response"); @@ -368,61 +373,57 @@ namespace Publishing.Authenticator.Shotwell.Google { // by the time we get a username, the session should be authenticated, or else something // really tragic has happened assert(session.is_authenticated()); - host.set_config_string("refresh_token", session.refresh_token); + try { + Secret.password_store_sync(this.schema, Secret.COLLECTION_DEFAULT, + "Shotwell publishing (Google account scope %s@%s)".printf(this.accountname, this.scope), + session.refresh_token, null, + SCHEMA_KEY_PROFILE_ID, host.get_current_profile_id(), + SCHEMA_KEY_ACCOUNTNAME, this.accountname, "scope", this.scope); + } catch (Error err) { + critical("Failed to look up password for scope %s: %s", this.scope, err.message); + } this.authenticated(); web_auth_pane.clear(); } - - private void do_exchange_refresh_token_for_access_token() { + private async void do_exchange_refresh_token_for_access_token() { debug("ACTION: exchanging OAuth refresh token for OAuth access token."); host.install_login_wait_pane(); RefreshAccessTokenTransaction txn = new RefreshAccessTokenTransaction(session); - - txn.completed.connect(on_refresh_access_token_transaction_completed); - txn.network_error.connect(on_refresh_access_token_transaction_error); - try { - txn.execute(); - } catch (Spit.Publishing.PublishingError err) { - host.post_error(err); - } - } - - private void on_refresh_access_token_transaction_completed(Publishing.RESTSupport.Transaction - txn) { - txn.completed.disconnect(on_refresh_access_token_transaction_completed); - txn.network_error.disconnect(on_refresh_access_token_transaction_error); + yield txn.execute_async(); + debug("EVENT: refresh access token transaction completed successfully."); - debug("EVENT: refresh access token transaction completed successfully."); - - if (session.is_authenticated()) // ignore these events if the session is already auth'd - return; - - do_extract_tokens(txn.get_response()); - } - - private void on_refresh_access_token_transaction_error(Publishing.RESTSupport.Transaction txn, - Spit.Publishing.PublishingError err) { - txn.completed.disconnect(on_refresh_access_token_transaction_completed); - txn.network_error.disconnect(on_refresh_access_token_transaction_error); - - debug("EVENT: refresh access token transaction caused a network error."); - - if (session.is_authenticated()) // ignore these events if the session is already auth'd - return; - if (txn.get_status_code() == Soup.Status.BAD_REQUEST || - txn.get_status_code() == Soup.Status.UNAUTHORIZED) { - // Refresh token invalid, starting over - host.set_config_string("refresh_token", ""); - Idle.add (() => { this.authenticate(); return false; }); + if (session.is_authenticated()) // ignore these events if the session is already auth'd + return; + + do_extract_tokens(txn.get_response()); + } catch (Error err) { + debug("EVENT: refresh access token transaction caused a network error."); + + if (session.is_authenticated()) // ignore these events if the session is already auth'd + return; + + if (txn.get_status_code() == Soup.Status.BAD_REQUEST || + txn.get_status_code() == Soup.Status.UNAUTHORIZED) { + // Refresh token invalid, starting over + try { + Secret.password_clear_sync(this.schema, null, + SCHEMA_KEY_PROFILE_ID, host.get_current_profile_id(), + SCHEMA_KEY_ACCOUNTNAME, this.accountname, "scope", this.scope); + } catch (Error err) { + critical("Failed to remove password for accountname@scope %s@%s: %s", this.accountname, this.scope, err.message); + } + + Idle.add (() => { this.authenticate(); return false; }); + } + + web_auth_pane.clear(); + host.post_error(err); } - - web_auth_pane.clear(); - host.post_error(err); } private void do_show_service_welcome_pane() { @@ -436,7 +437,5 @@ namespace Publishing.Authenticator.Shotwell.Google { this.do_hosted_web_authentication(); } - - } } |