diff options
Diffstat (limited to 'plugins/authenticator/shotwell/GoogleAuthenticator.vala')
-rw-r--r-- | plugins/authenticator/shotwell/GoogleAuthenticator.vala | 409 |
1 files changed, 409 insertions, 0 deletions
diff --git a/plugins/authenticator/shotwell/GoogleAuthenticator.vala b/plugins/authenticator/shotwell/GoogleAuthenticator.vala new file mode 100644 index 0000000..9271b57 --- /dev/null +++ b/plugins/authenticator/shotwell/GoogleAuthenticator.vala @@ -0,0 +1,409 @@ +using Shotwell; +using Shotwell.Plugins; + +namespace Publishing.Authenticator.Shotwell.Google { + private const string OAUTH_CLIENT_ID = "534227538559-hvj2e8bj0vfv2f49r7gvjoq6jibfav67.apps.googleusercontent.com"; + private const string OAUTH_CLIENT_SECRET = "pwpzZ7W1TCcD5uIfYCu8sM7x"; + + private class WebAuthenticationPane : Common.WebAuthenticationPane { + public static bool cache_dirty = false; + + public signal void authorized(string auth_code); + + public WebAuthenticationPane(string auth_sequence_start_url) { + Object (login_uri : auth_sequence_start_url); + } + + public static bool is_cache_dirty() { + return cache_dirty; + } + + public override void on_page_load() { + string page_title = get_view ().get_title(); + if (page_title.index_of("state=connect") > 0) { + int auth_code_field_start = page_title.index_of("code="); + if (auth_code_field_start < 0) + return; + + string auth_code = page_title.substring(auth_code_field_start + 5); // 5 = "code=".length + + cache_dirty = true; + + authorized(auth_code); + } + } + } + + private class Session : Publishing.RESTSupport.Session { + public string access_token = null; + public string refresh_token = null; + public int64 expires_at = -1; + + public override bool is_authenticated() { + return (access_token != null); + } + + public void deauthenticate() { + access_token = null; + refresh_token = null; + expires_at = -1; + } + } + + private class GetAccessTokensTransaction : Publishing.RESTSupport.Transaction { + private const string ENDPOINT_URL = "https://accounts.google.com/o/oauth2/token"; + + public GetAccessTokensTransaction(Session session, string auth_code) { + base.with_endpoint_url(session, ENDPOINT_URL); + + add_argument("code", auth_code); + add_argument("client_id", OAUTH_CLIENT_ID); + add_argument("client_secret", OAUTH_CLIENT_SECRET); + add_argument("redirect_uri", "urn:ietf:wg:oauth:2.0:oob"); + add_argument("grant_type", "authorization_code"); + } + } + + private class RefreshAccessTokenTransaction : Publishing.RESTSupport.Transaction { + private const string ENDPOINT_URL = "https://accounts.google.com/o/oauth2/token"; + + public RefreshAccessTokenTransaction(Session session) { + base.with_endpoint_url(session, ENDPOINT_URL); + + add_argument("client_id", OAUTH_CLIENT_ID); + add_argument("client_secret", OAUTH_CLIENT_SECRET); + add_argument("refresh_token", session.refresh_token); + add_argument("grant_type", "refresh_token"); + } + } + + private class UsernameFetchTransaction : Publishing.RESTSupport.Transaction { + private const string ENDPOINT_URL = "https://www.googleapis.com/oauth2/v1/userinfo"; + public UsernameFetchTransaction(Session session) { + base.with_endpoint_url(session, ENDPOINT_URL, Publishing.RESTSupport.HttpMethod.GET); + add_header("Authorization", "Bearer " + session.access_token); + } + } + + internal class Google : Spit.Publishing.Authenticator, Object { + private string scope = null; + 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; + + public Google(string scope, + string welcome_message, + Spit.Publishing.PluginHost host) { + this.host = host; + this.params = new GLib.HashTable<string, Variant>(str_hash, str_equal); + this.scope = scope; + this.session = new Session(); + this.welcome_message = welcome_message; + } + + public void authenticate() { + var refresh_token = host.get_config_string("refresh_token", null); + if (refresh_token != null && refresh_token != "") { + on_refresh_token_available(refresh_token); + do_exchange_refresh_token_for_access_token(); + return; + } + + // FIXME: Find a way for a proper logout + if (WebAuthenticationPane.is_cache_dirty()) { + host.set_service_locked(false); + + host.install_static_message_pane(_("You have already logged in and out of a Google service during this Shotwell session.\n\nTo continue publishing to Google services, quit and restart Shotwell, then try publishing again.")); + } else { + this.do_show_service_welcome_pane(); + } + } + + public bool can_logout() { + return true; + } + + public GLib.HashTable<string, Variant> get_authentication_parameter() { + return this.params; + } + + public void logout() { + session.deauthenticate(); + host.set_config_string("refresh_token", ""); + } + + public void refresh() { + // TODO: Needs to re-auth + } + + 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("urn:ietf:wg:oauth:2.0:oob", null) + "&" + + "scope=" + Soup.URI.encode(this.scope, null) + "+" + + Soup.URI.encode("https://www.googleapis.com/auth/userinfo.profile", null) + "&" + + "state=connect&" + + "access_type=offline&" + + "approval_prompt=force"; + + web_auth_pane = new WebAuthenticationPane(user_authorization_url); + web_auth_pane.authorized.connect(on_web_auth_pane_authorized); + + host.install_dialog_pane(web_auth_pane); + } + + private void on_web_auth_pane_authorized(string auth_code) { + web_auth_pane.authorized.disconnect(on_web_auth_pane_authorized); + + debug("EVENT: user authorized scope %s with auth_code %s", scope, auth_code); + + do_get_access_tokens(auth_code); + } + + private 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) { + 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) { + debug("ACTION: extracting OAuth tokens from body of server response"); + + Json.Parser parser = new Json.Parser(); + + try { + parser.load_from_data(response_body); + } catch (Error err) { + host.post_error(new Spit.Publishing.PublishingError.MALFORMED_RESPONSE( + "Couldn't parse JSON response: " + err.message)); + return; + } + + Json.Object response_obj = parser.get_root().get_object(); + + if ((!response_obj.has_member("access_token")) && (!response_obj.has_member("refresh_token"))) { + host.post_error(new Spit.Publishing.PublishingError.MALFORMED_RESPONSE( + "neither access_token nor refresh_token not present in server response")); + return; + } + + if (response_obj.has_member("expires_in")) { + var duration = response_obj.get_int_member("expires_in"); + var abs_time = GLib.get_real_time() + duration * 1000L * 1000L; + on_expiry_time_avilable(abs_time); + } + + if (response_obj.has_member("refresh_token")) { + string refresh_token = response_obj.get_string_member("refresh_token"); + + if (refresh_token != "") + on_refresh_token_available(refresh_token); + } + + if (response_obj.has_member("access_token")) { + string access_token = response_obj.get_string_member("access_token"); + + if (access_token != "") + on_access_token_available(access_token); + } + } + + private void on_refresh_token_available(string token) { + debug("EVENT: an OAuth refresh token has become available; token = '%s'.", token); + this.params.insert("RefreshToken", new Variant.string(token)); + + session.refresh_token = token; + } + + private void on_expiry_time_avilable(int64 abs_time) { + debug("EVENT: an OAuth access token expiry time became available; time = %'" + int64.FORMAT + + "'.", abs_time); + + session.expires_at = abs_time; + this.params.insert("ExpiryTime", new Variant.int64(abs_time)); + } + + + private void on_access_token_available(string token) { + debug("EVENT: an OAuth access token has become available; token = '%s'.", token); + + session.access_token = token; + this.params.insert("AccessToken", new Variant.string(token)); + + do_fetch_username(); + } + + private 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(); + } catch (Error err) { + 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"); + + Json.Parser parser = new Json.Parser(); + + try { + parser.load_from_data(response_body); + } catch (Error err) { + host.post_error(new Spit.Publishing.PublishingError.MALFORMED_RESPONSE( + "Couldn't parse JSON response: " + err.message)); + return; + } + + Json.Object response_obj = parser.get_root().get_object(); + + if (response_obj.has_member("name")) { + string username = response_obj.get_string_member("name"); + + if (username != "") + this.params.insert("UserName", new Variant.string(username)); + } + + if (response_obj.has_member("access_token")) { + string access_token = response_obj.get_string_member("access_token"); + + if (access_token != "") + this.params.insert("AccessToken", new Variant.string(access_token)); + } + + // 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); + + this.authenticated(); + } + + + private 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); + + 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; }); + } + + host.post_error(err); + } + + private void do_show_service_welcome_pane() { + debug("ACTION: showing service welcome pane."); + + this.host.install_welcome_pane(this.welcome_message, on_service_welcome_login); + } + + private void on_service_welcome_login() { + debug("EVENT: user clicked 'Login' in welcome pane."); + + this.do_hosted_web_authentication(); + } + + + } +} |