hello.js icon indicating copy to clipboard operation
hello.js copied to clipboard

Yahoo via OAuth2

Open Rah1x opened this issue 9 years ago • 29 comments

Hi,

I have done it myself for Yahoo via its new OAuth2 (https://developer.yahoo.com/oauth2/guide/#implicit-grant-flow-for-client-side-apps).

The Authentication is working just fine, and can get you the access token. However, the Contact API is not working as Yahoo server is not returning the correct Access-Control-Allow-Origin header cause the browser to block the Cross-Origin xhr call.

So the only solution was to take the Access Token and follow the rest via CURL (where it works just fine).

CONCEPTS:

  1. Creating API Access keys from Yahoo.com will require you to insert .com in your localhost. So make a vhost that has .com

  2. Yahoo's OAuth server will not accept such large URL and hence return an error the details of which are not so listed anywhere. So except for state>callback parameter, rest all are passed to the processing page via Session and reinserted into hello.js upon successful authentication.

Rah1x avatar Feb 04 '15 14:02 Rah1x

Code: Modified modules/yahoo.js

//
// Yahoo
//
// Register Yahoo developer
(function(hello){

function formatError(o){
    if(o && "meta" in o && "error_type" in o.meta){
        o.error = {
            code : o.meta.error_type,
            message : o.meta.error_message
        };
    }
}

function formatFriends(o,headers,request){
    //alert(dump(headers));
    formatError(o);
    paging(o, headers, request);
    var contact,field;
    if(o.query&&o.query.results&&o.query.results.contact){
        o.data = o.query.results.contact;
        delete o.query;
        if(!(o.data instanceof Array)){
            o.data = [o.data];
        }
        for(var i=0;i<o.data.length;i++){
            contact = o.data[i];
            contact.id = null;
            for(var j=0;j<contact.fields.length;j++){
                field = contact.fields[j];
                if(field.type === 'email'){
                    contact.email = field.value;
                }
                if(field.type === 'name'){
                    contact.first_name = field.value.givenName;
                    contact.last_name = field.value.familyName;
                    contact.name = field.value.givenName + ' ' + field.value.familyName;
                }
                if(field.type === 'yahooid'){
                    contact.id = field.value;
                }
            }
        }
    }
    return o;
}

function paging(res, headers, request){

    // PAGING
    // http://developer.yahoo.com/yql/guide/paging.html#local_limits
    if(res.query && res.query.count && request.options ){
        res['paging'] = {
            next : '?start='+ ( res.query.count + ( +request.options.start || 1 ) )
        };
    }
}

var yql = function(q){
    return 'https://query.yahooapis.com/v1/yql?q=' + (q + ' limit @{limit|100} offset @{start|0}').replace(/\s/g, '%20') + "&format=json";
};

hello.init({
    'yahoo' : {
        // Ensure that you define an oauth_proxy
        /*oauth : {
            version : "1.0a",
            //version : "2.0",
            auth    : "https://api.login.yahoo.com/oauth/v2/request_auth",
            request : 'https://api.login.yahoo.com/oauth/v2/get_request_token',
            token   : 'https://api.login.yahoo.com/oauth/v2/get_token'
        },*/

        oauth: {
            version : 2,
            auth    : "https://api.login.yahoo.com/oauth2/request_auth",
            request : 'https://api.login.yahoo.com/oauth2/get_request_token',
            token   : 'https://api.login.yahoo.com/oauth2/get_token',
        },

        // Login handler
        login : function(p){
            // Change the default popup window to be atleast 560
            // Yahoo does dynamically change it on the fly for the signin screen (only, what if your already signed in)
            p.options.window_width = 560;
        },
        /*
        // AUTO REFRESH FIX: Bug in Yahoo can't get this to work with node-oauth-shim
        login : function(o){
            // Is the user already logged in
            var auth = hello('yahoo').getAuthResponse();

            // Is this a refresh token?
            if(o.options.display==='none'&&auth&&auth.access_token&&auth.refresh_token){
                // Add the old token and the refresh token, including path to the query
                // See http://developer.yahoo.com/oauth/guide/oauth-refreshaccesstoken.html
                o.qs.access_token = auth.access_token;
                o.qs.refresh_token = auth.refresh_token;
                o.qs.token_url = 'https://api.login.yahoo.com/oauth/v2/get_token';
            }
        },
        */

        base    : "https://social.yahooapis.com/v1/user/",

        get : {
            "me"        : yql('select * from social.profile(0) where guid=me'),
            //"me/friends"  : yql('select * from social.contacts where guid=me'),
            "me/friends"    : 'me/contacts',
            "me/following"  : yql('select * from social.contacts(0) where guid=me')
        },
        wrap : {
            me : function(o){
                formatError(o);
                if(o.query&&o.query.results&&o.query.results.profile){
                    o = o.query.results.profile;
                    o.id = o.guid;
                    o.last_name = o.familyName;
                    o.first_name = o.givenName || o.nickname;
                    var a = [];
                    if(o.first_name){
                        a.push(o.first_name);
                    }
                    if(o.last_name){
                        a.push(o.last_name);
                    }
                    o.name = a.join(' ');
                    o.email = o.emails?o.emails.handle:null;
                    o.thumbnail = o.image?o.image.imageUrl:null;
                }
                return o;
            },
            // Can't get ID's
            // It might be better to loop through the social.relationshipd table with has unique ID's of users.
            "me/friends" : formatFriends,
            "me/following" : formatFriends,
            "default" : function(res){
                paging(res);
                return res;
            }
        },

        xhr : function(p)
        {
            //alert(dump(p.options.access_token));
            /*if( p.method !== 'get' && p.method !== 'delete' && !hello.utils.hasBinary(p.data) ){

                // Does this have a data-uri to upload as a file?
                if( typeof( p.data.file ) === 'string' ){
                    p.data.file = hello.utils.toBlob(p.data.file);
                }else{
                    p.data = JSON.stringify(p.data);
                    p.headers = {
                        'Content-Type' : 'application/json'
                    };
                }
            }*/
            if(typeof(p.options.access_token)!='undefined'){
            p.headers = {
                'Authorization' : 'Bearer '+p.options.access_token
            };
            }
            return true;
        },
    }
});

})(hello);

Rah1x avatar Feb 04 '15 14:02 Rah1x

Hi Raheel that's great. However I can't merge it without a pull request. Thanks

MrSwitch avatar Feb 04 '15 15:02 MrSwitch

The reason I didnt create a pull-request is that this is partial solution as it will rely on Curl via PHP or any other code. The reason being that is though our work is complete, but the Yahoo server is not returning a crucial header that allows cross-browser ajax call.

Rah1x avatar Feb 04 '15 15:02 Rah1x

That's fine, the oauth-proxy settings can easily be modified to enforce that a passthru requests.

Thanks On 5 Feb 2015 00:34, "Raheel Hsn" [email protected] wrote:

The reason I didnt create a pull-request is that this is partial solution as it will rely on Curl via PHP or any other code. The reason being that is though our work is complete, but the Yahoo server is not returning a crucial header that allows cross-browser ajax call.

— Reply to this email directly or view it on GitHub https://github.com/MrSwitch/hello.js/issues/192#issuecomment-72875905.

MrSwitch avatar Feb 04 '15 23:02 MrSwitch

ok.. well the reason I used OAuth2.0 instead was that I didnt want to use the proxy.. Otherwise, OAuth1 via proxy would have just worked fine...

Also, tell me which branch do you want me to create the pull request?

Thanks..

Rah1x avatar Feb 06 '15 08:02 Rah1x

The oauth-proxy is already doing CORS polyfills for other networks.

Git can merge any branch into MrSwitch/master so however you want to manage it.

Thanks

MrSwitch avatar Feb 06 '15 08:02 MrSwitch

@Rah1x did you create a PR?

MrSwitch avatar Feb 28 '15 07:02 MrSwitch

Hey, I just wanted to chime in and say that I noticed that the Yahoo Social APIs are now listed under the OAuth 2.0 Guide: https://developer.yahoo.com/oauth2/guide/ (March 10th, 2015). It wasn't there the last time I checked (March 6th 2015). So, maybe Yahoo's OAuth2 APIs for Contacts w/ implicit grant is fully working now i.e. no need for proxy anymore?

newtonapple avatar Mar 10 '15 22:03 newtonapple

@newtonapple that's great

I just did a rudimentary test, i found an issue with the state parameter character limit,

i.e. this wont work

https://api.login.yahoo.com/oauth2/request_auth?client_id=dj0yJmk9cjVDdHlDaGtrbldJJmQ9WVdrOVYyZFhSWE4yTm04bWNHbzlNQS0tJnM9Y29uc3VtZXJzZWNyZXQmeD1jOA--&response_type=token&redirect_uri=http%3A%2F%2Flocal.knarly.com%2Fhello.js%2Fredirect.html&display=popup&state=%7B%22client_id%22%3A%22dj0yJmk9cjVDdHlDaGtrbldJJmQ9WVdrOVYyZFhSWE4yTm04bWNHbzlNQS0tJnM9Y29uc3VtZXJzZWNyZXQmeD1jOA--%22%2C%22network%22%3A%22yahoo%22%2C%22display%22%3A%22popup%22%2C%22callback%22%3A%22_hellojs_22urf8xp%22%2C%22state%22%3A%22%22%2C%22redirect_uri%22%3A%22http%3A%2F%2Flocal.knarly.com%2Fhello.js%2Fredirect.html%22%7D

but this will

https://api.login.yahoo.com/oauth2/request_auth?client_id=dj0yJmk9cjVDdHlDaGtrbldJJmQ9WVdrOVYyZFhSWE4yTm04bWNHbzlNQS0tJnM9Y29uc3VtZXJzZWNyZXQmeD1jOA--&response_type=token&redirect_uri=http%3A%2F%2Flocal.knarly.com%2Fhello.js%2Fredirect.html&display=popup&state=%7B%22client_id%22%3A%22dj0yJmk9cjVDdHlDaGtrbldJJmQ9WVdrOVYyZFhSWE4yTm04bWNHbzlNQS0tJnM9Y29uc3VtZXJzZWNyZXQmeD1jOA--%22%2C%22network%22%3A%22yahoo%22%2C%22display%22%3A%22popup%22%2C%22callback%22%3A%22_hellojs_22urf8xp%22

Reported to https://hackerone.com/reports/51106

MrSwitch avatar Mar 11 '15 12:03 MrSwitch

+1 Awesome, looking forward to this!

mikelewis avatar Mar 11 '15 19:03 mikelewis

Yet another critical issue with yahoo's OAuth2 state parameter...

The state field is formatted such that the encoded double quote character ("), aka (%22) is converted to a plus (+) on the return

e.g.

Request:

https://api.login.yahoo.com/oauth2/request_auth?client_id=dj0yJmk9cjVDdHlDaGtrbldJJmQ9WVdrOVYyZFhSWE4yTm04bWNHbzlNQS0tJnM9Y29uc3VtZXJzZWNyZXQmeD1jOA--&response_type=token&redirect_uri=http%3A%2F%2Flocal.knarly.com%2Fhello.js%2Fredirect.html&display=popup&state=%7B%22network%22:%22yahoo%22,%22display%22:%22popup%22,%22callback%22:%22_hellojs_2rb21r8x%22,%22state%22:%22%22%7D

Response:

http://local.knarly.com/hello.js/redirect.html#access_token=token&token_type=bearer&state=%7B+network+%3A+yahoo+%2C+display+%3A+popup+%2C+callback+%3A+_hellojs_2rb21r8x+%2C+state+%3A+%7D&xoauth_yahoo_guid=HROVGK6OHIIAXUNKG7KXOWUBVQ

MrSwitch avatar Mar 12 '15 09:03 MrSwitch

So where does that leave us?

mikelewis avatar Mar 16 '15 16:03 mikelewis

@mikelewis i'm waiting for Yahoo to respond to the bug report.

The state data could be saved in alternative ways, but i'm hoping yahoo will fix this soon.

MrSwitch avatar Mar 16 '15 22:03 MrSwitch

Hey there, I finally had a chance to play around with Yahoo's OAuth2 API. To summarize for everyone, basically, there are 3 problems:

  1. the state parameter only accepts value with limited characters length.
  2. the state param returned in the callback redirect does not encode double quote characters properly (+ instead of %22)
  3. Cross origin AJAX response to an API endpoint still DOESN'T contain the necessary CORS header Access-Control-Allow-Origin.

Problem 1 & 2 are easily solvable by encoding the state differently and leaving out some of the longer params already known in app like client_id and callback URL. Problem 3, however, will need changes on Yahoo's end. One option to get around this is to proxy the AJAX request to Yahoo from your own domain. We are planning on doing this via Nginx. This really isn't ideal, but it'll work if you really want to use the OAuth2 API and not run the Node.js OAuth1 proxy.

I really hope Yahoo will get their isht together and fix all these problems soon...

newtonapple avatar Mar 24 '15 02:03 newtonapple

@sahat can you shed light on this Auth issue with Yahoo OAuth2? (and keep your impressive 559 days streak on Github going :+1: )

MrSwitch avatar Mar 24 '15 03:03 MrSwitch

No response from my bug report at https://hackerone.com/yahoo?.

If we all retweeet https://twitter.com/setData/status/580215026771881986 perhaps Yahoo Developer Network might get on this thread.

MrSwitch avatar Mar 24 '15 03:03 MrSwitch

@MrSwitch I'm from the Yahoo Developer Network team. Thanks for the report! We're looking into this issue.

saurabhsahni avatar Mar 24 '15 04:03 saurabhsahni

Thanks @saurabhsahni

MrSwitch avatar Mar 24 '15 05:03 MrSwitch

@MrSwitch Thats why in my solution, I had except for state>callback parameter all passed to the processing page via PHP Session and reinserted into hello.js upon successful authentication

Rah1x avatar Apr 02 '15 09:04 Rah1x

please help me.. i m getting "Could not decode state parameter" after yahoo login...

rajakumar1991 avatar Oct 29 '15 15:10 rajakumar1991

After getting inconsistent result with existing yahoo implementation. I got fed up and started implementing yahoo oauth 2.0 implementation by keeping the yahoo branch as base. Reached a point where i can access the data through chrome where i disabled the securities. Now the cross domain issue is haunting me. After doing so many things and tried so many methods it's a disappointment to see the developers didn't added cross domain access to api calls which was supposed to be called from different domain.

A simple change in hello.js

 try {
                var pState = p.state.replace(/&quot;/g, '\"');
                var a = JSON.parse(pState);
                _this.extend(p, a);
            }
            catch (e) {
                console.error('Could not decode state parameter');
            }

Current yahoo.js

//
// Yahoo
//
// Register Yahoo developer
(function (hello) {

    hello.init({
        'yahoo': {
            // Ensure that you define an oauth_proxy
            oauth: {
                version: 2,
                auth: "https://api.login.yahoo.com/oauth2/request_auth",
                grant: 'https://api.login.yahoo.com/oauth2/get_token'
            },

            // Login handler
            login: function (p) {
                // Change the default popup window to be atleast 560
                // Yahoo does dynamically change it on the fly for the signin screen (only, what if your already signed in)
                p.options.window_width = 560;


                // Yahoo throws an parameter error if for whatever reason the state.scope contains a comma, so lets remove scope
                try { delete p.qs.scope; delete p.qs.state.scope; } catch (e) { }
            },
            /*
            // AUTO REFRESH FIX: Bug in Yahoo can't get this to work with node-oauth-shim
            login : function(o){
                // Is the user already logged in
                var auth = hello('yahoo').getAuthResponse();

                // Is this a refresh token?
                if(o.options.display==='none'&&auth&&auth.access_token&&auth.refresh_token){
                    // Add the old token and the refresh token, including path to the query
                    // See http://developer.yahoo.com/oauth/guide/oauth-refreshaccesstoken.html
                    o.qs.access_token = auth.access_token;
                    o.qs.refresh_token = auth.refresh_token;
                    o.qs.token_url = 'https://api.login.yahoo.com/oauth/v2/get_token';
                }
            },
            */

            base: "https://social.yahooapis.com/v1/",

            get: {
                "me": yql('select * from social.profile where guid=me'),
                "me/friends": yql('SELECT * FROM social.contacts(0) WHERE guid=me'),
                "me/following": yql('select * from social.contacts where guid=me')
            },
            wrap: {
                me: formatUser,
                // Can't get ID's
                // It might be better to loop through the social.relationshipd table with has unique ID's of users.
                "me/friends": formatFriends,
                "me/following": formatFriends,
                "default": paging
            },

            xhr: function (p) {
                //alert(dump(p.options.access_token));
                /*if( p.method !== 'get' && p.method !== 'delete' && !hello.utils.hasBinary(p.data) ){

                    // Does this have a data-uri to upload as a file?
                    if( typeof( p.data.file ) === 'string' ){
                        p.data.file = hello.utils.toBlob(p.data.file);
                    }else{
                        p.data = JSON.stringify(p.data);
                        p.headers = {
                            'Content-Type' : 'application/json'
                        };
                    }
                }*/
                alert("hi");
                if (typeof (p.options.access_token) != 'undefined') {
                    p.headers = {
                        'Authorization': 'Bearer ' + p.options.access_token
                    };
                }
                return true;
            }
        }
    });

    function formatError(o) {
        if (o && 'meta' in o && 'error_type' in o.meta) {
            o.error = {
                code: o.meta.error_type,
                message: o.meta.error_message
            };
        }
    }

    function formatUser(o) {

        formatError(o);
        if (o.query && o.query.results && o.query.results.profile) {
            o = o.query.results.profile;
            o.id = o.guid;
            o.last_name = o.familyName;
            o.first_name = o.givenName || o.nickname;
            var a = [];
            if (o.first_name) {
                a.push(o.first_name);
            }

            if (o.last_name) {
                a.push(o.last_name);
            }

            o.name = a.join(' ');
            o.email = (o.emails && o.emails[0]) ? o.emails[0].handle : null;
            o.thumbnail = o.image ? o.image.imageUrl : null;
        }

        return o;
    }

    function formatFriends(o, headers, request) {
        formatError(o);
        paging(o, headers, request);
        var contact;
        var field;
        if (o.query && o.query.results && o.query.results.contact) {
            o.data = o.query.results.contact;
            delete o.query;

            if (!Array.isArray(o.data)) {
                o.data = [o.data];
            }

            o.data.forEach(formatFriend);
        }

        return o;
    }

    function formatFriend(contact) {
        contact.id = null;

        // #362: Reports of responses returning a single item, rather than an Array of items.
        // Format the contact.fields to be an array.
        if (contact.fields && !(contact.fields instanceof Array)) {
            contact.fields = [contact.fields];
        }

        (contact.fields || []).forEach(function (field) {
            if (field.type === 'email') {
                contact.email = field.value;
            }

            if (field.type === 'name') {
                contact.first_name = field.value.givenName;
                contact.last_name = field.value.familyName;
                contact.name = field.value.givenName + ' ' + field.value.familyName;
            }

            if (field.type === 'yahooid') {
                contact.id = field.value;
            }
        });
    }

    function paging(res, headers, request) {

        // See: http://developer.yahoo.com/yql/guide/paging.html#local_limits
        if (res.query && res.query.count && request.options) {
            res.paging = {
                next: '?start=' + (res.query.count + (+request.options.start || 1))
            };
        }

        return res;
    }

    function yql(q) {
        return 'https://query.yahooapis.com/v1/yql?q=' + (q + ' limit @{limit|100} offset @{start|0}').replace(/\s/g, '%20') + '&format=json';
    }



})(hello);

ratheeshkannan avatar Dec 17 '15 17:12 ratheeshkannan

@ratheeshkannan Yahoo seems to have gone quiet on this one.

MrSwitch avatar Dec 18 '15 10:12 MrSwitch

@MrSwitch With no information from Yahoo team, I've asked the Backend team member to provide me a service which carries the parameters like auth_token, limit and offset which in turn he'll use it to hit yahoo server and give me back whatever that is coming from the yahoo server.

If you can, can you please help me in implementing it without modifying the core functionalities of hello.js. The flow is after getting the authorization token. I need to call my server (post) with necessary parameters and send the result to hello.js normal workflow.

How to interrupt the normal workflow of hello.js xhr call and overwrite it in yahoo.js so that i can insert a block of my code which should overwrite the normal workflow. I don't want to change anything in hello.js. But i can if necessary.

ratheeshkannan avatar Dec 18 '15 13:12 ratheeshkannan

@ratheeshkannan what can you do with OAuth2 that you can't do with the current OAuth1 version? - since both have their drawbacks right now.

MrSwitch avatar Dec 18 '15 13:12 MrSwitch

In OAuth1 version, I'm getting empty response many times (#364). Which is very much annoying but in Oauth2 I'm getting a 100% hit to yahoo servers and getting result consistently. So i think it's better to rely on Oauth2 rather than Oauth1.

ratheeshkannan avatar Dec 18 '15 13:12 ratheeshkannan

I'm having no luck retrieving contacts with several solutions listed here. Is there any progress made on this?

Can I currently get contacts via yahoo?

fabrizio5680 avatar Jan 20 '16 17:01 fabrizio5680

@MrSwitch what is the status of this issue? Yahoo has started to have issues with OAuth 1 for their APIs and are advising to use 2...

isaacrlevin avatar Sep 19 '17 14:09 isaacrlevin

@isaac2004 i've marked this as Good First Issue, hoping someone will investigate the latest Yahoo API and rework the Hello Yahoo module accordingly.

MrSwitch avatar Oct 19 '17 07:10 MrSwitch