summaryrefslogtreecommitdiff
path: root/plugins/authenticator/shotwell/GoogleAuthenticator.vala
diff options
context:
space:
mode:
Diffstat (limited to 'plugins/authenticator/shotwell/GoogleAuthenticator.vala')
-rw-r--r--plugins/authenticator/shotwell/GoogleAuthenticator.vala409
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();
+ }
+
+
+ }
+}