Google PeopleAPI & HttpListener Redirect URI

177 Views Asked by At

We have an a WPF app that allows the user to import their Google contacts using the PeopleAPI. We're modifying it because Google recently changed their auth process. See here

The idea is to call the Google Auth page and use an HTTPListener to listen on a port for the redirect URI. Google has a WPF example here.

First I went into the Google Control panel and changed the RedirectURI from the page we were using to https://127.0.0.1.

Next I set up the HTTPListener to handle the callback. The problem is that by using a random port number, how can you specify a callback URI in the control panel?

I've tried multiple things. Here's my code. In this interation, I set the callback URI in the Google Control Panel to 127.0.0.1 and pass it as the callback in the request. But the listener is set to use a random port, so the two redirect URI's arnt really the same:

public async Task TryAuthorizeAsync()
{
    // Generates state and PKCE values.
    string state = RandomDataBase64url(32);
    string codeVerifier = RandomDataBase64url(32);
    string codeChallenge = Base64urlencodeNoPadding(SHA256(codeVerifier));

    // Creates an HttpListener to listen for requests
    string localHost = $"https://{IPAddress.Loopback}";
    string listenerURI = $"{localHost}:{GetRandomUnusedPort()}/"; // Includes a random port #
    var listener = new HttpListener();
    RaiseStatusChanged($"listening on {listenerURI}");
    listener.Prefixes.Add(listenerURI);

    listener.Start();

    // Create the authorization request passing <a href="https://127.0.0.1">https://127.0.0.1</a> as the redirect.
    var authorizationRequest = $"{AuthorizationEndpoint}?response_type=code&" +
                                $"scope=openid%20profile&" +
                                $"redirect_uri={Uri.EscapeDataString(localHost)}&" +
                                $"client_id={ClientID}&" +
                                $"state={state}&" +
                                $"code_challenge={codeChallenge}&" +
                                $"code_challenge_method={CODE_CHALLENEGE_METHOD}";

    // Opens request in the default browser
    Process.Start(authorizationRequest);

    bool success = false;

    // Wait for the auth authorization response.
    await Task.Run(() =>
    {
        // Begin waiting for context. Call the ListenerCallback when context is recieved
        IAsyncResult context2 = listener.BeginGetContext(new AsyncCallback(result => ListenerCallback(result, state, codeVerifier, listenerURI)), listener);

        if (HANDLE_TIMEOUT)
        {
            success = context2.AsyncWaitHandle.WaitOne(RESPONSE_TIMEOUT, true);
        }
    });

    // If here and both handling the timeout and the auth was not successfull, then the user
    // didn't do anything in the browser, so the auth process timed out. If the user did
    // anything in the browser, the ListenerCallback method handles it.
    if (HANDLE_TIMEOUT && !success)
    {
        RaiseAuthorizationTimedOut();
    }
}

When I run this, the user's Google auth page opens in the browser and when the user selects their account and authorizes, but the ListenerCallback never fires.

What I do see is the page directs to

This site can't be reached
127.0.0.1 refused to connect

Agian, the problem seems to be is that the auth needs the redirect port number. But we can't use a dedicated port number in the Google API Console, so how then can I redirect to the listener method?

1

There are 1 best solutions below

4
VonC On

Strange: the HttpListener is listening on a random port, as you coded here:

string listenerURI = $"{localHost}:{GetRandomUnusedPort()}/";

But the redirect URI that you are passing in the authorization request is just the localhost address without any port:

$"redirect_uri={Uri.EscapeDataString(localHost)}&" +

You should make sure that the redirect URI specified in the Google API Console, the redirect URI passed in the authorization request, and the URI your HttpListener is listening on are all the same.


Note: The same consistency should apply to the scheme of the redirect URI is consistent. If you are using https in the Google API Console, then the HttpListener must also be configured to use https, and vice versa. If you are using https, you will also need to handle the SSL certificate. For local testing, it might be easier to use http instead.


Since googlesamples/oauth-apps-for-windows OAuthDesktopApp/OAuthDesktopApp/MainWindow.xaml.cs is using a random port, you should dynamically obtain the port number assigned to the HttpListener and update the redirect URI accordingly.

The problem is that we are prohibited from using a port number in the console. And the console won't accept wildcards. That's the whole issue.

And, how is their sample app doing it?? If they're using a random port number, then what could possibly be in their console for this? They can't have listed a port number. And it won't take wildcards.

That Google's sample app OAuthDesktopApp/OAuthDesktopApp/MainWindow.xaml.cs likely uses a different approach called "out-of-band" (OOB) URI. This is a special redirect URI that tells the Google authorization server to return the authorization code in the title bar of the page, rather than sending it to a web server. Desktop apps can use this approach to obtain the authorization code without having to listen on a specific port.

The idea is, in the Google API Console, to set the redirect URI for your OAuth 2.0 client ID to urn:ietf:wg:oauth:2.0:oob.

Then modify your authorization request to use the OOB URI as the redirect URI:

var authorizationRequest = $"{AuthorizationEndpoint}?response_type=code&" +
                            $"scope=openid%20profile&" +
                            $"redirect_uri=urn:ietf:wg:oauth:2.0:oob&" +
                            $"client_id={ClientID}&" +
                            $"state={state}&" +
                            $"code_challenge={codeChallenge}&" +
                            $"code_challenge_method={CODE_CHALLENEGE_METHOD}";

However, since Feb. 2022, such a legacy flow has been deprecated.

That is where you realize the random port, in that Google example, is 7 years old. And references a 2010 Stack Overflow question.

Just for your information, Google uses your code in all their OAuth codes and examples :) (Aug. 2017)

In other words, you no longer can use that example as-is.
You would need to migrate to another workflow, like OAuth 2.0 for TV and Limited-Input Device Applications or follow the "Making Google OAuth interactions safer by using more secure OAuth flows " (Feb. 2022) other recommendations.


The OP CoderForHire adds in the comments:

It turns out that this works:

"string listenerURI = $"127.0.0.1:{GetRandomUnusedPort()}/";" 

It seems that they use 127.0.0.1 and add on whatever port you're listening on.

It seems like Google has allowed the use of 127.0.0.1 with varying port numbers, which is a viable solution for desktop applications where the port number might change.

Using 127.0.0.1 with a random unused port obtained by GetRandomUnusedPort() as the loopback address for the HttpListener is an approach that allows you to have a dynamic redirect URI for the OAuth 2.0 authorization code flow.

Meaning:

  • Your application creates an HttpListener that listens on a random unused port on 127.0.0.1.
  • You construct the authorization request URL and specify the redirect URI to be 127.0.0.1 with the port that your HttpListener is listening on.
  • The user authorizes the application, and Google redirects to 127.0.0.1 with the specified port.
  • Your HttpListener captures the redirect, and you can extract the authorization code from the redirect URI.

This approach is effective for local development and for applications that are not publicly exposed. It is also a common pattern for OAuth 2.0 authorization in desktop applications.