diff -Nru gsignond-plugin-oauth-0.0.2~r70+pkg3~ubuntu0.3.1/debian/bzr-builder.manifest gsignond-plugin-oauth-0.0.2~r71+pkg3~ubuntu0.3.1/debian/bzr-builder.manifest --- gsignond-plugin-oauth-0.0.2~r70+pkg3~ubuntu0.3.1/debian/bzr-builder.manifest 2014-06-04 16:02:35.000000000 +0000 +++ gsignond-plugin-oauth-0.0.2~r71+pkg3~ubuntu0.3.1/debian/bzr-builder.manifest 2014-06-05 20:42:45.000000000 +0000 @@ -1,3 +1,3 @@ -# bzr-builder format 0.3 deb-version {debupstream}~r70+pkg3 -lp:gsignond-plugin-oauth revid:git-v1:f66a028c94156d41abb4d53450d6c1e35b417952 +# bzr-builder format 0.3 deb-version {debupstream}~r71+pkg3 +lp:gsignond-plugin-oauth revid:git-v1:cb0dfc149a7bbcb21eae9955df50cd7b4c9e8ccb nest-part packaging lp:~elementary-os/gsignond-plugin-oauth/deb-packaging debian debian revid:tintou@mailoo.org-20140309113100-amr39zfccyfxm46e diff -Nru gsignond-plugin-oauth-0.0.2~r70+pkg3~ubuntu0.3.1/debian/changelog gsignond-plugin-oauth-0.0.2~r71+pkg3~ubuntu0.3.1/debian/changelog --- gsignond-plugin-oauth-0.0.2~r70+pkg3~ubuntu0.3.1/debian/changelog 2014-06-04 16:02:35.000000000 +0000 +++ gsignond-plugin-oauth-0.0.2~r71+pkg3~ubuntu0.3.1/debian/changelog 2014-06-05 20:42:45.000000000 +0000 @@ -1,8 +1,8 @@ -gsignond-plugin-oauth (0.0.2~r70+pkg3~ubuntu0.3.1) trusty; urgency=low +gsignond-plugin-oauth (0.0.2~r71+pkg3~ubuntu0.3.1) trusty; urgency=low * Auto build. - -- Launchpad Package Builder Wed, 04 Jun 2014 16:02:35 +0000 + -- Launchpad Package Builder Thu, 05 Jun 2014 20:42:45 +0000 gsignond-plugin-oauth (0.0.2-1) unstable; urgency=low diff -Nru gsignond-plugin-oauth-0.0.2~r70+pkg3~ubuntu0.3.1/src/gsignond-oauth-plugin-oauth2.c gsignond-plugin-oauth-0.0.2~r71+pkg3~ubuntu0.3.1/src/gsignond-oauth-plugin-oauth2.c --- gsignond-plugin-oauth-0.0.2~r70+pkg3~ubuntu0.3.1/src/gsignond-oauth-plugin-oauth2.c 2014-06-04 16:02:33.000000000 +0000 +++ gsignond-plugin-oauth-0.0.2~r71+pkg3~ubuntu0.3.1/src/gsignond-oauth-plugin-oauth2.c 2014-06-05 20:42:42.000000000 +0000 @@ -214,20 +214,8 @@ return params; } -static void -_http_token_callback (SoupSession *session, SoupMessage *msg, gpointer user_data) +static GHashTable* _parse_json_response(SoupMessage* msg, GError** error) { - GError* error = NULL; - GSignondOauthPlugin *self = GSIGNOND_OAUTH_PLUGIN(user_data); - - if (msg->status_code != SOUP_STATUS_OK && msg->status_code != SOUP_STATUS_BAD_REQUEST) { - error = g_error_new(GSIGNOND_ERROR, - GSIGNOND_ERROR_NOT_AUTHORIZED, - "Token endpoint returned an error: %d %s", - msg->status_code, msg->reason_phrase); - goto out; - } - SoupBuffer* request = soup_message_body_flatten(msg->response_body); JsonParser* parser = json_parser_new(); @@ -235,23 +223,63 @@ soup_buffer_free(request); if (res == FALSE) { g_object_unref(parser); - error = g_error_new(GSIGNOND_ERROR, + *error = g_error_new(GSIGNOND_ERROR, GSIGNOND_ERROR_NOT_AUTHORIZED, "Json parser returned an error"); - goto out; + return NULL; } if (json_node_get_node_type(json_parser_get_root(parser)) != JSON_NODE_OBJECT) { g_object_unref(parser); - error = g_error_new(GSIGNOND_ERROR, + *error = g_error_new(GSIGNOND_ERROR, GSIGNOND_ERROR_NOT_AUTHORIZED, "Json top-level structure is not an object"); - goto out; + return NULL; } GHashTable* params = _get_json_params(json_node_get_object(json_parser_get_root(parser))); g_object_unref(parser); + return params; +} + +static GHashTable* _parse_urlencoded_response(SoupMessage* msg, GError** error) +{ + SoupBuffer* request = soup_message_body_flatten(msg->response_body); + GHashTable* params = soup_form_decode(request->data); + soup_buffer_free(request); + return params; +} + +static void +_http_token_callback (SoupSession *session, SoupMessage *msg, gpointer user_data) +{ + GError* error = NULL; + GSignondOauthPlugin *self = GSIGNOND_OAUTH_PLUGIN(user_data); + if (msg->status_code != SOUP_STATUS_OK && msg->status_code != SOUP_STATUS_BAD_REQUEST) { + error = g_error_new(GSIGNOND_ERROR, + GSIGNOND_ERROR_NOT_AUTHORIZED, + "Token endpoint returned an error: %d %s", + msg->status_code, msg->reason_phrase); + goto out; + } + + const gchar* content_type = soup_message_headers_get_content_type(msg->response_headers, NULL); + GHashTable* params = NULL; + if (g_strcmp0(content_type, "application/json") == 0) + params = _parse_json_response(msg, &error); + // facebook and github return token in form-urlencoded form (&-separated values) + else if ((g_strcmp0(content_type, "text/plain") == 0) || + (g_strcmp0(content_type, "application/x-www-form-urlencoded") == 0)) + params = _parse_urlencoded_response(msg, &error); + else + error = g_error_new(GSIGNOND_ERROR, + GSIGNOND_ERROR_NOT_AUTHORIZED, + "Unknown content type in response: %s", + content_type); + if (error != NULL) + goto out; + // if using a refresh token failed, go back to full authentication process // using supplied credentials info const gchar* oauth_error = g_hash_table_lookup(params, "error"); @@ -708,12 +736,21 @@ static GHashTable* _get_token_params(GHashTable* params, const gchar* token_type) { GHashTable* token_params = NULL; + + // facebook doesn't use a token type at all, which is a clear violation of OAuth standard + // https://developers.facebook.com/docs/facebook-login/manually-build-a-login-flow/v2.0#exchangecode + if (token_type == NULL) { + token_params = g_hash_table_new((GHashFunc)g_str_hash, + (GEqualFunc)g_str_equal); + } + // 'bearer' is used by microsoft: // http://msdn.microsoft.com/en-us/library/live/hh243641.aspx#signin if (g_strcmp0(token_type, "Bearer") == 0 || g_strcmp0(token_type, "bearer") == 0) { token_params = g_hash_table_new((GHashFunc)g_str_hash, (GEqualFunc)g_str_equal); } + return token_params; } @@ -741,7 +778,9 @@ } GSignondDictionary* token_dict = gsignond_dictionary_new(); gsignond_dictionary_set_string(token_dict, "AccessToken", access_token); - gsignond_dictionary_set_string(token_dict, "TokenType", token_type); + + if (token_type) + gsignond_dictionary_set_string(token_dict, "TokenType", token_type); gsignond_dictionary_set(token_dict, "TokenParameters", gsignond_dictionary_to_variant( additional_token_params)); @@ -752,6 +791,10 @@ g_date_time_unref(now); const gchar* duration_s = g_hash_table_lookup(params, "expires_in"); + // try also 'expires'; that's what facebook is using: + // https://developers.facebook.com/docs/facebook-login/manually-build-a-login-flow/v2.0#exchangecode + if (duration_s == NULL) + duration_s = g_hash_table_lookup(params, "expires"); if (duration_s != NULL) { gchar* endptr; gint64 duration = g_ascii_strtoll(duration_s, &endptr, 10); diff -Nru gsignond-plugin-oauth-0.0.2~r70+pkg3~ubuntu0.3.1/test/oauth2tests.c gsignond-plugin-oauth-0.0.2~r71+pkg3~ubuntu0.3.1/test/oauth2tests.c --- gsignond-plugin-oauth-0.0.2~r70+pkg3~ubuntu0.3.1/test/oauth2tests.c 2014-06-04 16:02:33.000000000 +0000 +++ gsignond-plugin-oauth-0.0.2~r71+pkg3~ubuntu0.3.1/test/oauth2tests.c 2014-06-05 20:42:42.000000000 +0000 @@ -654,32 +654,6 @@ g_error_free(error); error = NULL; - //access token exists, but no token type - gsignond_plugin_request_initial(plugin, data, tokens, "oauth2"); - fail_if(result != NULL); - fail_if(ui_action == NULL); - gsignond_dictionary_unref(ui_action); - ui_action = NULL; - fail_if(store != NULL); - fail_if(error != NULL); - params = soup_form_encode("state", - gsignond_dictionary_get_string(data, "_Oauth2State"), - "access_token", "megatoken", - NULL); - url = g_strdup_printf("http://somehost/login.html#%s", params); - gsignond_signonui_data_set_url_response(ui_data, url); - g_free(url); - g_free(params); - gsignond_plugin_user_action_finished(plugin, ui_data); - fail_if(result != NULL); - fail_if(ui_action != NULL); - fail_if(store != NULL); - fail_if(error == NULL); - fail_unless(g_error_matches(error, GSIGNOND_ERROR, - GSIGNOND_ERROR_NOT_AUTHORIZED)); - g_error_free(error); - error = NULL; - // unknown token type gsignond_plugin_request_initial(plugin, data, tokens, "oauth2"); fail_if(result != NULL); @@ -2142,6 +2116,234 @@ } END_TEST +static void +facebook_token_server_callback (SoupServer *server, + SoupMessage *msg, + const char *path, + GHashTable *query, + SoupClientContext *client, + gpointer user_data) +{ + const gchar* full_token_response = "access_token=My-Token&expires_in=5184000&token_type=Bearer&refresh_token=new-refresh-token"; + const gchar* facebook_token_response = "access_token=My-Facebook-Token&expires=3600"; + + fail_if(g_str_has_prefix (path, "/tokenpath") == FALSE); + fail_if(g_strcmp0(msg->method, "POST") != 0); + fail_if(g_strcmp0(soup_message_headers_get_content_type( + msg->request_headers, NULL), "application/x-www-form-urlencoded") != 0); + + SoupBuffer* request = soup_message_body_flatten(msg->request_body); + GHashTable* params = soup_form_decode(request->data); + soup_buffer_free(request); + fail_if(g_strcmp0(g_hash_table_lookup(params, "grant_type"), "authorization_code") != 0); + fail_if(g_strcmp0(g_hash_table_lookup(params, "code"), "mega-auth-code") != 0); + fail_if(g_strcmp0(g_hash_table_lookup(params, "redirect_uri"), "http://somehost/login.html") != 0); + fail_if(g_strcmp0(g_hash_table_lookup(params, "client_id"), "megaclient") != 0); + g_hash_table_unref(params); + + if (g_strrstr(path, "facebook") != NULL) { + soup_message_set_status (msg, SOUP_STATUS_OK); + soup_message_set_response (msg, "text/plain;charset=UTF-8", + SOUP_MEMORY_STATIC, + facebook_token_response, strlen(facebook_token_response)); + } else { + soup_message_set_status (msg, SOUP_STATUS_OK); + soup_message_set_response (msg, "application/x-www-form-urlencoded;charset=UTF-8", + SOUP_MEMORY_STATIC, + full_token_response, strlen(full_token_response)); + } +} + +START_TEST (test_oauth2_facebook) +{ + SoupServer* server = soup_server_new(SOUP_SERVER_SSL_CERT_FILE, "cacert.pem", + SOUP_SERVER_SSL_KEY_FILE, "privkey.pem", + NULL); + soup_server_add_handler (server, "/tokenpath", facebook_token_server_callback, + NULL, NULL); + soup_server_run_async(server); + + gpointer plugin; + + plugin = g_object_new(GSIGNOND_TYPE_OAUTH_PLUGIN, NULL); + fail_if(plugin == NULL); + + GSignondSessionData* result = NULL; + GSignondSessionData* store = NULL; + GSignondSignonuiData* ui_action = NULL; + GError* error = NULL; + + //gsize len; + gint64 expires_in; + gchar* url; + gchar* params; + + g_signal_connect(plugin, "response-final", G_CALLBACK(response_callback), &result); + g_signal_connect(plugin, "user-action-required", + G_CALLBACK(user_action_required_callback), &ui_action); + g_signal_connect(plugin, "store", G_CALLBACK(store_callback), &store); + g_signal_connect(plugin, "error", G_CALLBACK(error_callback), &error); + + GSignondSessionData* data = gsignond_dictionary_new(); + GSignondDictionary* tokens = make_tokens("someclient", make_expired_token()); + GSignondSignonuiData* ui_data = gsignond_dictionary_new(); + + gsignond_dictionary_set_string(data, "ClientId", "megaclient"); + gsignond_session_data_set_ui_policy(data, GSIGNOND_UI_POLICY_DEFAULT); + gsignond_dictionary_set_string(data, "TokenHost", "localhost"); + gsignond_dictionary_set_string(data, "TokenPath", "/tokenpath"); + gsignond_dictionary_set_uint32(data, "TokenPort", soup_server_get_port(server)); + gsignond_dictionary_set_string(data, "Scope", "scope1 scope3"); + + gsignond_dictionary_set_boolean(data, "SslStrict", FALSE); + + gsignond_dictionary_set_string(data, "AuthHost", "somehost"); + gsignond_dictionary_set_string(data, "AuthPath", "/somepath"); + gsignond_dictionary_set_string(data, "ResponseType", "code"); + gsignond_dictionary_set_string(data, "RedirectUri", "http://somehost/login.html"); + const gchar *realm_list[] = { "localhost", "somehost", NULL }; + GSequence* allowed_realms = gsignond_copy_array_to_sequence(realm_list); + gsignond_session_data_set_allowed_realms(data, allowed_realms); + g_sequence_free(allowed_realms); + + //server returns full form-urlencoded response + gsignond_plugin_request_initial(plugin, data, tokens, "oauth2"); + fail_if(result != NULL); + fail_if(ui_action == NULL); + gsignond_dictionary_unref(ui_action); + ui_action = NULL; + fail_if(store != NULL); + fail_if(error != NULL); + + params = soup_form_encode("state", + gsignond_dictionary_get_string(data, "_Oauth2State"), + "code", "mega-auth-code", + NULL); + url = g_strdup_printf("http://somehost/login.html?%s", params); + gsignond_signonui_data_set_url_response(ui_data, url); + gsignond_signonui_data_set_query_error(ui_data, SIGNONUI_ERROR_NONE); + g_free(url); + g_free(params); + gsignond_plugin_user_action_finished(plugin, ui_data); + + while (1) { + g_main_context_iteration(g_main_context_default(), TRUE); + if(result != NULL) + break; + } + fail_if(result == NULL); + fail_if(g_strcmp0(gsignond_dictionary_get_string(result, "AccessToken"), + "My-Token") != 0); + fail_if(g_strcmp0(gsignond_dictionary_get_string(result, "RefreshToken"), + "new-refresh-token") != 0); + fail_if(g_strcmp0(gsignond_dictionary_get_string(result, "TokenType"), + "Bearer") != 0); + fail_if(gsignond_dictionary_get_int64(result, "Duration", &expires_in) != TRUE); + fail_if(expires_in != 5184000); + fail_if(g_strcmp0(gsignond_dictionary_get_string(result, "Scope"), "scope1 scope3") != 0); + + gsignond_dictionary_unref(result); + result = NULL; + fail_if(ui_action != NULL); + fail_if(store == NULL); + GSignondDictionary* client_tokens = gsignond_dictionary_new_from_variant( + gsignond_dictionary_get(store, "megaclient")); + GSignondDictionary* token = gsignond_dictionary_new_from_variant( + gsignond_dictionary_get(client_tokens, "scope1 scope3")); + fail_if(token == NULL); + + fail_if(g_strcmp0(gsignond_dictionary_get_string(token, "AccessToken"), + "My-Token") != 0); + fail_if(g_strcmp0(gsignond_dictionary_get_string(token, "RefreshToken"), + "new-refresh-token") != 0); + fail_if(g_strcmp0(gsignond_dictionary_get_string(token, "TokenType"), + "Bearer") != 0); + fail_if(gsignond_dictionary_get_int64(token, "Duration", &expires_in) != TRUE); + fail_if(expires_in != 5184000); + fail_if(g_strcmp0(gsignond_dictionary_get_string(token, "Scope"), "scope1 scope3") != 0); + + gsignond_dictionary_unref(token); + gsignond_dictionary_unref(client_tokens); + + gsignond_dictionary_unref(store); + store = NULL; + fail_if(error != NULL); + + // try with abbreviated facebook token + gsignond_dictionary_unref(tokens); + tokens = make_tokens("someclient", make_expired_token()); + gsignond_dictionary_set_string(data, "TokenPath", "/tokenpath/facebook"); + + gsignond_plugin_request_initial(plugin, data, tokens, "oauth2"); + fail_if(result != NULL); + fail_if(ui_action == NULL); + gsignond_dictionary_unref(ui_action); + ui_action = NULL; + fail_if(store != NULL); + fail_if(error != NULL); + + params = soup_form_encode("state", + gsignond_dictionary_get_string(data, "_Oauth2State"), + "code", "mega-auth-code", + NULL); + url = g_strdup_printf("http://somehost/login.html?%s", params); + gsignond_signonui_data_set_url_response(ui_data, url); + g_free(url); + g_free(params); + + gsignond_plugin_user_action_finished(plugin, ui_data); + + while (1) { + g_main_context_iteration(g_main_context_default(), TRUE); + if(result != NULL) + break; + } + //g_print("%s\n", error->message); + fail_if(result == NULL); + + fail_if(g_strcmp0(gsignond_dictionary_get_string(result, "AccessToken"), + "My-Facebook-Token") != 0); + fail_if(gsignond_dictionary_get_string(result, "RefreshToken") != NULL); + fail_if(gsignond_dictionary_get_string(result, "TokenType") != NULL); + fail_if(gsignond_dictionary_get_int64(result, "Duration", &expires_in) != TRUE); + fail_if(expires_in != 3600); + fail_if(g_strcmp0(gsignond_dictionary_get_string(result, "Scope"), "scope1 scope3") != 0); + + gsignond_dictionary_unref(result); + result = NULL; + fail_if(ui_action != NULL); + fail_if(store == NULL); + client_tokens = gsignond_dictionary_new_from_variant( + gsignond_dictionary_get(store, "megaclient")); + token = gsignond_dictionary_new_from_variant( + gsignond_dictionary_get(client_tokens, "scope1 scope3")); + fail_if(token == NULL); + + fail_if(g_strcmp0(gsignond_dictionary_get_string(token, "AccessToken"), + "My-Facebook-Token") != 0); + fail_if(gsignond_dictionary_get_string(token, "RefreshToken") != NULL); + fail_if(gsignond_dictionary_get_string(token, "TokenType") != NULL); + fail_if(gsignond_dictionary_get_int64(token, "Duration", &expires_in) != TRUE); + fail_if(expires_in != 3600); + fail_if(g_strcmp0(gsignond_dictionary_get_string(token, "Scope"), "scope1 scope3") != 0); + + gsignond_dictionary_unref(token); + gsignond_dictionary_unref(client_tokens); + + gsignond_dictionary_unref(store); + store = NULL; + fail_if(error != NULL); + + + gsignond_dictionary_unref(ui_data); + gsignond_dictionary_unref(data); + gsignond_dictionary_unref(tokens); + g_object_unref(plugin); + g_object_unref(server); +} +END_TEST + + //printf("%s\n",g_variant_print(gsignond_dictionary_to_variant(store), TRUE)); void add_oauth2_tcase(Suite *s) @@ -2157,6 +2359,7 @@ tcase_add_test (tc_oauth2, test_oauth2_owner_password); tcase_add_test (tc_oauth2, test_oauth2_client_credentials); tcase_add_test (tc_oauth2, test_oauth2_authorization_code); + tcase_add_test (tc_oauth2, test_oauth2_facebook); suite_add_tcase (s, tc_oauth2); }