4

The Stack Exchange API has this Javascript library for authentication.

What do I pass as channelUrl for a user script running on SE sites?

The docs said it needed to be "a blank page on the same domain as the including page", and since I can't put a blank page on SE sites (obviously) the best I could do was something on the same domain.

So I tried passing document.location.origin (e.g. http://english.stackexchange.com or whatever) but after the login window pops up and the authorization is accepted it shows "unexpected domain on proxy url", which I'm guessing is related to channelUrl:

          Screenshot of error

Also the docs say channelUrl "is used for cross-domain communication in older browsers", but for some reason it must be specified and is still checked, even though I really don't care about "older browsers"...

3
  • According to this it has to match the registered domain for your app, maybe that's all you need and it doesn't have to be on the same domain "as the including page"?
    – Cai
    Apr 7, 2017 at 13:49
  • @AwesomePoodles I asked there yesterday. It received no views, then I noticed the barren wasteland of support there, deleted it, and moved it here.
    – Jason C
    Apr 7, 2017 at 14:32
  • @Cai Nope, I tried that first actually. If I pass one on the registered domain, SE.init fails with "channelUrl must be under the current domain", and I don't even get as far as being able to attempt to authenticate.
    – Jason C
    Apr 7, 2017 at 14:38

1 Answer 1

3

SE's Javascript SDK assumes that you have your own domain and that everything is same-domain. The SDK has code like:

// proxyUrl must be under the currently hosting domain
if (proxyUrl.toLowerCase().indexOf(domain) !== 0) {
    throw 'channelUrl must be under the current domain';
}

in its source.

In a nutshell:

  1. channelUrl must be on the same domain as the calling page. Hard to do if you don't own that domain. :)
  2. Your app's OAuth Domain setting must be the same domain as channelUrl. Again difficult for a userscript, and impossible if the userscript runs on several domains -- like a general purpose SE script must.
  3. Alternatively, the OAuth Domain can be stackexchange.com (more on this below), but this conflicts with item 1 of this list.

Solutions:

Either:

  • Buy and operate your own domain, allowing you to use the, limited usefulness, "Javascript SDK".
  • Or trust some third party with your (and your user's) sacred OAuth bits. I'm sure ArtOfCode is a standup guy, but what happens when his server is hacked or the wife wins it all in a divorce, etc.? :P
  • Or Roll your own OAuth code. A sample and framework follows:

A basic framework for cross-domain API authentication in a userscript:

Setup:

A sample script with the framework:

  • WARNING: The following works but has not yet been tested as much as I prefer.
  • The script will run as-is, but you should change the first three consts to match your own app.
  • The script operates in a standard-ish parent/child configuration.
  • See code comments for more.
// ==UserScript==
// @name        StackExchange, API authenticate framework

// These are the target page matches. Start with the baseline Stack Exchange set. Refine to taste.
// @match       *://*.askubuntu.com/*
// @match       *://*.serverfault.com/*
// @match       *://*.stackapps.com/*
// @match       *://*.stackexchange.com/*
// @match       *://*.stackoverflow.com/*
// @match       *://*.superuser.com/*

// The following match is required for authentication to work.
// @match       https://stackexchange.com/oauth/login_success*

// Baseline Stack Exchange excludes.
// @exclude     *://api.stackexchange.com/*
// @exclude     *://blog.stackexchange.com/*
// @exclude     *://blog.stackoverflow.com/*
// @exclude     *://chat.stackexchange.com/*
// @exclude     *://chat.stackoverflow.com/*
// @exclude     *://data.stackexchange.com/*
// @exclude     *://elections.stackexchange.com/*
// @exclude     *://openid.stackexchange.com/*
// @exclude     *://*/review
// @require     http://ajax.googleapis.com/ajax/libs/jquery/2.1.0/jquery.min.js
// @reference   https://meta.stackexchange.com/a/293498/148310
// @grant       GM_setValue
// @grant       GM_getValue
// ==/UserScript==

/*--- This script operates in one of two modes.  Either on a main (target) page, or in an API Oauth dialog if required.
*/
const apiID         = 1515;   //-- Change this to your app's
const apikey        = "dp55hR5Wr9UhOJReA6F2gg((";   //-- Change this to your app's
const authScope     = "read_inbox";   //-- Change this to your needs

const apiBaseURL    = "https://api.stackexchange.com/2.2";
var accessToken     = null;

function scriptMain () {
    //--- Place the payload code here. For example:

    //-- Use the /inbox route:
    var reqURL = apiBaseURL + `/inbox?filter=!6LdGvjJhl2HHw&key=${apikey}&access_token=${accessToken}`;
    $.getJSON (reqURL, parseMeInbox).fail ( function (jqXHR, textStatus) {
        console.error ("API error: ", textStatus);
        console.error ("Detail: ", jqXHR.responseText);
        GM_setValue ("APIToken", "");
    } );
}

function parseMeInbox (zData) {
    if (zData.items && zData.items.length) {
        console.log ("Your last message was: ", zData.items[0].body);
    }
    else {
        console.log ("No inbox messages found for you.");
    }

    //-- Always check for (and ideally handle) these errors:
    if (zData.backoff)
        console.error (`API sent backoff warning, ${zData.backoff} seconds.`);
    if (zData.quota_remaining < 20)
        console.error (`API low quota alert. ${zData.quota_remaining} remaining.`);
    if (zData.error_id) {
        console.error ( `
            Error ${zData.error_id}, ${zData.error_name}.
            ${zData.error_message}
        ` );

        //-- On authentication type errors, attempt to reauthenticate.
        if (zData.error_id == 403) {
            GM_setValue ("APIToken", "");
            authorizeAndThenRunMain ();
        }
    }
}

/****************************************************************************************
*** Authentication processing code follows.  Should not need alteration.
*/
if (location.pathname == "/oauth/login_success") {
    processOauthPopup ();
}
else {
    authorizeAndThenRunMain ();
}

function authorizeAndThenRunMain () {
    //-- Only run max 2 times, to prevent bad auth loops.
    let self = authorizeAndThenRunMain;
    self.runcnt = self.runcnt || 0;
    if (self.runcnt++ > 1)  return;

    var authWindow  = null;
    accessToken     = GM_getValue ("APIToken");
    if (accessToken) {
        /*-- Can check token to make sure it's still good.
            See https://stackapps.com/a/7105/7653  and  https://stackapps.com/a/4980/7653
            BUT more quota efficient to make payload call and only reauthenticate when get 403 error.
        */
        scriptMain ();
    }
    else {
        authWindow = window.open (
            `https://stackexchange.com/oauth/dialog?client_id=${apiID}&scope=${authScope}&redirect_uri=https://stackexchange.com/oauth/login_success`,
            "GM to API Auth window",
            "resizable,scrollbars,status,toolbar,dependent,width=660,height=480"
        );
        if (!authWindow) {
            alert ("This script requires popups to be enabled.")
            throw "This script requires popups to be enabled.";
        }
        window.addEventListener ("message", receiveAPI_Token);
    }
}

function receiveAPI_Token (zEvent) {
    if (zEvent.origin != "https://stackexchange.com")     return;

    //#access_token=jvH3nWQGH*********59*w))&expires=86399
    var aTknMtch = zEvent.data.match (/access_token=([^&]+)&?/);
    if (aTknMtch  &&  aTknMtch.length > 1) {
        accessToken = aTknMtch[1];
        GM_setValue ("APIToken", accessToken);

        scriptMain ();
    }
    else {
        console.error ("Unable to authenticate!  Error: ", zEvent.data);
    }
}

function processOauthPopup () {
    if (window.opener) {
        if (location.hash) {
            window.opener.postMessage (location.hash, "*");
            window.close ();
        }
        else {
            window.opener.postMessage ('Unknown error with Oauth Dialog.', "*");
            console.error ('Unknown error with Oauth Dialog.');
        }
    }
}
13
  • First, +1, this is awesome, and works great. Second, I'm going to wait on accepting this until I get to the bottom of something else: Motivated by your answer I started doing some digging in to api.stackexchange.com/js/2.0/all.js; and I (unexpectedly) seem to have narrowed this down to a potential bug / issue in /oauth/login_success (in particular, not only is the access_token actually available even on error, but it doesn't seem to be posting window messages even on error as it should). I'm doing some investigation and will post back. I'm sure there's a proper SE-side fix.
    – Jason C
    Apr 7, 2017 at 23:53
  • Actually, no, on second thought. I'm going to accept this. It's a great workaround. Then I'm going to open a new bug report if appropriate once I get to the bottom of what's really going on here (I'm partial to fixing root causes).
    – Jason C
    Apr 7, 2017 at 23:54
  • The root cause is that the "SDK" deliberately has that domain check. Remove it and the SDK can be made to work (Although, still maybe not from a userscript). But it only does that one thing, and not well ("You had one job"). And the code is not open-source. Best to just use your own framework and move on. ... The SDK and other more universal login problems have languished for years. I really think that they thought one of us would step up with a proper JS library by now. :) Apr 8, 2017 at 0:16
  • No that's not it. I tried that and there's a lot more going on. It's boiling down to /oauth/login_success issues. Btw, minor issue, I had to change your access token regex to /access_token=([^&]+)/ (remove the final &).
    – Jason C
    Apr 8, 2017 at 0:18
  • Also, you need to cache the valid tokens in a cross-domain way. No SDK JS can do that. You need a userscript engine like GM and TM. Apr 8, 2017 at 0:18
  • 1
    Re: "I had to change your access token regex". Did you request a "no expiry" token? Also that change will cause problems in some cases. Try the updated code above. Apr 8, 2017 at 0:22
  • I did use no_expiry. Can confirm, new regex (&?) also works.
    – Jason C
    Apr 8, 2017 at 0:27
  • What's boggling my mind about the SE API, is that even if I drop out channelUrl altogether, and kill the state parameter to /oauth/dialog, and drop all the parameters (assisted, protocol, proxy) to /oauth/login_success, and verify that the window.open URL is sane and that the message handler is being installed, login_success still doesn't send the message despite becoming successful. It's really stumping me. It's either something basic I'm missing or something wrong with login_success. The SE API doesn't call the error callback on the original failure, either, same issue.
    – Jason C
    Apr 8, 2017 at 0:38
  • (Prettified SE API is at pastebin.com/sFJQEvD6, it's super straightforward aside from the minification, I'm trying to "fix" it using your framework as a reference).
    – Jason C
    Apr 8, 2017 at 0:41
  • 1
    Technically the SDK, not the API. Anywho, the code really does have designed in problems with cross domain. I spent a lot of time looking at it once and my conclusion was "FUBAR; only even tries to do a small part the job; start afresh." Apr 8, 2017 at 0:42
  • Sure, but it essentially does the exact same thing: Open the popup, handle the message, grab the token. I mean, your framework and the SDK (lol) don't have much difference there, at least for the initial authorization: window.open /oauth/dialog, forward to /oauth/login_success, install message handler, get data. Anyways, yeah, I may give up eventually. It's just bothering me...
    – Jason C
    Apr 8, 2017 at 0:44
  • Re: "Prettified SE API is at...". Why? The gen-u-wine source is at: dev.sstatic.net/apiv2/js/all.js ... Also there are lots of other problems if you are trying to fix the "SDK". Example: stackapps.com/questions/4733/… Apr 8, 2017 at 0:47
  • 1
    Let us continue this discussion in chat. Apr 8, 2017 at 0:49

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .